From 346bc511190b2677e14b8b7a3e3e6a24728b4733 Mon Sep 17 00:00:00 2001 From: Noah Meyerhans Date: Wed, 16 Sep 2026 15:06:35 -0400 Subject: [PATCH] Import dovecot_2.4.1+dfsg1-6+deb13u7.debian.tar.xz [dgit import tarball dovecot 1:2.4.1+dfsg1-6+deb13u7 dovecot_2.4.1+dfsg1-6+deb13u7.debian.tar.xz] --- autogen.sh | 6 + changelog | 3143 +++++++++++++++++ conf/conf.d/10-auth.conf | 120 + conf/conf.d/10-logging.conf | 86 + conf/conf.d/10-mail.conf | 410 +++ conf/conf.d/10-master.conf | 133 + conf/conf.d/10-metrics.conf | 122 + conf/conf.d/10-ssl.conf | 56 + conf/conf.d/15-lda.conf | 50 + conf/conf.d/15-mailboxes.conf | 86 + conf/conf.d/20-imap.conf | 108 + conf/conf.d/20-lmtp.conf | 51 + conf/conf.d/20-managesieve.conf | 78 + conf/conf.d/20-pop3.conf | 101 + conf/conf.d/20-submission.conf | 114 + conf/conf.d/30-dict-server.conf | 34 + conf/conf.d/90-acl.conf | 30 + conf/conf.d/90-fts-flatcurve.conf | 29 + conf/conf.d/90-fts.conf | 26 + conf/conf.d/90-quota.conf | 79 + conf/conf.d/90-sieve-extprograms.conf | 41 + conf/conf.d/90-sieve.conf | 113 + conf/conf.d/99-local.conf | 0 conf/conf.d/auth-deny.conf.ext | 15 + conf/conf.d/auth-ldap.conf.ext | 48 + conf/conf.d/auth-master.conf.ext | 12 + conf/conf.d/auth-oauth2.conf.ext | 23 + conf/conf.d/auth-passwdfile.conf.ext | 20 + conf/conf.d/auth-sql.conf.ext | 180 + conf/conf.d/auth-static.conf.ext | 35 + conf/conf.d/auth-system.conf.ext | 72 + conf/dovecot.conf | 87 + conf/local.conf | 0 control | 304 ++ copyright | 266 ++ dh.pem | 13 + dovecot-core.NEWS | 193 + dovecot-core.README.Debian | 35 + dovecot-core.bug-control | 1 + dovecot-core.bug-script | 28 + dovecot-core.dirs | 4 + dovecot-core.docs | 2 + dovecot-core.dovecot.default | 4 + dovecot-core.dovecot.init | 185 + dovecot-core.dovecot.pam | 6 + dovecot-core.install | 3 + dovecot-core.lintian-overrides | 59 + dovecot-core.manpages | 1 + dovecot-core.postinst | 95 + dovecot-core.postrm | 48 + dovecot-core.triggers | 2 + dovecot-dev.README.Debian | 10 + dovecot-dev.lintian-overrides | 2 + dovecot-flatcurve.links | 1 + dovecot-flatcurve.postinst | 21 + dovecot-flatcurve.postrm | 25 + dovecot-flatcurve.prerm | 12 + dovecot-flatcurve.triggers | 2 + dovecot-gssapi.links | 1 + dovecot-imapd.links | 1 + dovecot-imapd.lintian-overrides | 2 + dovecot-imapd.postinst | 23 + dovecot-imapd.postrm | 29 + dovecot-imapd.prerm | 12 + dovecot-imapd.triggers | 1 + dovecot-imapd.ufw.profile | 11 + dovecot-ldap.links | 1 + dovecot-ldap.postinst | 21 + dovecot-ldap.postrm | 27 + dovecot-ldap.triggers | 2 + dovecot-lmtpd.links | 1 + dovecot-lmtpd.postinst | 23 + dovecot-lmtpd.postrm | 29 + dovecot-lmtpd.prerm | 12 + dovecot-lmtpd.triggers | 2 + dovecot-managesieved.README.Debian | 17 + dovecot-managesieved.install | 1 + dovecot-managesieved.links | 1 + dovecot-managesieved.lintian-overrides | 2 + dovecot-managesieved.postinst | 23 + dovecot-managesieved.postrm | 29 + dovecot-managesieved.prerm | 12 + dovecot-managesieved.triggers | 2 + dovecot-mysql.links | 1 + dovecot-mysql.triggers | 2 + dovecot-openssl.cnf | 23 + dovecot-pgsql.links | 1 + dovecot-pgsql.triggers | 2 + dovecot-pop3d.links | 1 + dovecot-pop3d.postinst | 23 + dovecot-pop3d.postrm | 29 + dovecot-pop3d.prerm | 12 + dovecot-pop3d.triggers | 2 + dovecot-pop3d.ufw.profile | 11 + dovecot-sieve.links | 1 + dovecot-sieve.lintian-overrides | 4 + dovecot-sieve.postinst | 18 + dovecot-sieve.postrm | 24 + dovecot-sieve.triggers | 2 + dovecot-solr.README.Debian | 2 + dovecot-solr.install | 1 + dovecot-solr.links | 1 + dovecot-solr.triggers | 2 + dovecot-sqlite.links | 1 + dovecot-sqlite.triggers | 2 + dovecot-submissiond.postinst | 27 + dovecot-submissiond.postrm | 29 + dovecot-submissiond.prerm | 12 + dovecot-submissiond.triggers | 1 + gbp.conf | 14 + maildirmake.dovecot | 28 + maildirmake.dovecot.1 | 46 + ...educe-nesting-in-token_in_scope-via-.patch | 71 + ...GIN_PROXY_FAILURE_TYPE_AUTH-to-LOGIN.patch | 143 + ...ntially-excessive-data-stack-growth-.patch | 40 + patches/0001-global-Fix-spelling.patch | 34 + ...e-channel-ostream-creation-into-help.patch | 82 + ...ap-Restrict-COMPRESS-to-DEFLATE-only.patch | 67 + ...ix-out-of-bounds-read-when-parsing-D.patch | 35 + ...omments-to-internal-ID-command-param.patch | 33 + ...xcessive-memory-growth-with-pre-logi.patch | 86 + ...ER-environment-if-d-hasn-t-been-spec.patch | 38 + ...01-lib-Add-XXH64-hash-implementation.patch | 331 ++ .../0001-lib-Add-str_equals_timing_safe.patch | 124 + ...Don-t-calculate-crc32-if-using-zlib-.patch | 45 + ...istream-lz4-Ensure-uncompressed-chun.patch | 98 + ...istream-lz4-Try-again-if-no-data-was.patch | 60 + ...-istream-zlib-Use-container_of-macro.patch | 56 + ...wrap-_read-in-for-loop-reindent-only.patch | 611 ++++ ..._fd-Document-that-it-preserves-errno.patch | 25 + ...tch-Fix-excessive-CPU-usage-caused-b.patch | 524 +++ ...-Fix-leaking-uninitialized-memory-in.patch | 35 + ...iver-Autocreate-delivery-mailbox-via.patch | 63 + ...harset-translation-buffer-between-MI.patch | 105 + ...ib-mail-o_stream_dot_sendv-Use-I_MIN.patch | 23 + ...-ostream-dot-Optimize-stream-writing.patch | 60 + ...managesieve-parser-Fix-handling-of-l.patch | 30 + ...r-user-sieve-rusage-file-for-cumulat.patch | 595 ++++ ...s-Split-mailbox-creation-into-sieve_.patch | 124 + ...binary-code-Fix-single-byte-oob-stac.patch | 41 + ...ve-sieve-storage-Add-is_personal-bit.patch | 72 + ...storage-Fix-panic-caused-by-flawed-d.patch | 53 + ...e-file-Refuse-symlinks-escaping-pers.patch | 267 ++ ...dit-mail-Fix-writing-to-freed-memory.patch | 128 + ...-sql-Add-sql_result_new_error-helper.patch | 66 + ...hr-based-while-loop-to-fill-template.patch | 100 + ...qlite-Use-sqlite3_snprintf-to-quote-.patch | 51 + ...Implement-async-calls-for-drivers-th.patch | 247 ++ ...w-NULL-path-in-mailbox_list_mkdir_ro.patch | 44 + ...ad-Avoid-excessive-data-stack-growth.patch | 55 + ...0001-lib-test-Add-test_assert_memcmp.patch | 104 + ...e-both-lengths-in-test_assert_memcmp.patch | 45 + ...te-ordering-bug-on-big-endian-system.patch | 45 + ..._PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED.patch | 99 + ...-crash-when-XCLIENT-FORWARD-base64-c.patch | 42 + ...ent-common-Add-client_disconnect-vfu.patch | 36 + ...in-Add-proxy_dest_connection_limit-e.patch | 134 + ...n-Adjust-to-core-s-login_proxy_failu.patch | 30 + ...n-Support-LOGIN_PROXY_FAILURE_TYPE_A.patch | 12 + ...ettings-leak-on-error-paths-in-clien.patch | 33 + ...x-event-in-trash_try_mailbox-for-set.patch | 63 + ...ng-forward_fields-without-a-value-fr.patch | 30 + ...ix-scope-check-to-require-all-config.patch | 49 + ...aking-doveadm_password-or-doveadm_ap.patch | 49 + ...-connect-Don-t-hang-when-server-reje.patch | 120 + ...ng-mail-or-attribute-value-that-begi.patch | 37 + ...ltiplex-ostream-side-channel-after-u.patch | 33 + ...STRUCTURE-to-client-without-ostream-.patch | 79 + ...se-imap-parser-API-for-parsing-DONE-.patch | 152 + ..._home-HOME-environment-if-not-using-.patch | 34 + ...ace-str_hash-strcase_hash-with-xxh64.patch | 80 + ...Iterate-instead-of-recursing-on-zero.patch | 187 + ...am_dot_sendv-Fix-boundary-off-by-one.patch | 62 + ...d-t_openat_safe-for-jailed-openat-re.patch | 406 +++ ...inary-header-resource-usage-tracking.patch | 304 ++ ...s-Create-fileinto-mailbox-without-th.patch | 146 + ...e-file-Validate-script-stat-path-thr.patch | 129 + ...est-edit-mail-Adjust-to-changes-in-d.patch | 104 + ...ct-sql_query_callback_delayed-helper.patch | 53 + ...ve-MAILBOX_FLAG_AUTO_CREATE-and-MAIL.patch | 70 + ...lify-mailbox_mkdir-using-mailbox_lis.patch | 31 + ...ad-Limit-References-header-msgid-cou.patch | 47 + ...id-array_front-panic-on-empty-forwar.patch | 50 + ...n-client_skip_line-Discard-data-when.patch | 29 + ...-client-Fix-panic-occurring-at-mail_.patch | 78 + ...dd-oauth2_audience-setting-deprecate.patch | 131 + ...-Delay-failing-if-used-COMPRESS-algo.patch | 41 + ...LOPE-to-client-without-ostream-memor.patch | 75 + patches/0003-lib-Add-str_stable_hash.patch | 43 + patches/0003-lib-Add-t_openat_safe_dir.patch | 129 + ...sion-Add-o_stream_deflate_reset_dict.patch | 104 + ...istream-zstd-Guard-against-no-progre.patch | 39 + ...m_dot_sendv-Use-enumeration-to-defin.patch | 91 + ...-on-disk-header-rusage-fields-to-unu.patch | 71 + ...ail_headers_parse-Fix-info-leak-via-.patch | 271 ++ ...rver-Expose-smtp_server_connection_r.patch | 51 + ...d-sql_commit_schedule_delayed-helper.patch | 50 + ...ad-Limit-ancestor-chain-traversal-de.patch | 92 + ...list-is-never-supposed-to-be-a-symli.patch | 26 + ...ict_reset-side-channel-command-to-im.patch | 55 + ...-strcase_hash-with-a-random-seed-to-.patch | 120 + ...test-Add-zero-len-frame-test-for-all.patch | 72 + ...-parser-from-parts-Enforce-50-MB-all.patch | 52 + ...m_dot_sendv-Do-not-send-unguarded-.-.patch | 216 ++ ...-sieve_max_cpu_time-enforcement-to-t.patch | 294 ++ ...hange-escape-func-signature-to-retur.patch | 305 ++ ...-client-authenticate-Reply-421-4.7.0.patch | 51 + ...map_compress_on_proxy-hidden-setting.patch | 62 + ...-about-memory-allocations-and-string.patch | 81 + ...ndex-strmap-Fix-OOB-read-from-trunca.patch | 27 + ...nt-field-to-struct-message_address_l.patch | 56 + ...torage-Remove-orphan-compiled-binari.patch | 55 + ...ape_string-return-int-with-error_r-f.patch | 629 ++++ ...-submission-proxy-Recognize-421-4.7..patch | 50 + ...connect-before-expanding-query-varia.patch | 35 + ...TE-dictionary-after-every-tagged-rep.patch | 61 + ...-crc32-variables-parameters-to-hash-.patch | 366 ++ ...warning-when-cumulative-CPU-exceeds-.patch | 55 + ...-Explicitly-disable-the-header-block.patch | 50 + ...yed-xxh64-strmap-format-v2-gated-by-.patch | 364 ++ ...-header-filter-Use-container_of-for-.patch | 65 + ...-info-line-when-opening-a-CPU-disabl.patch | 52 + ...statement-queries-in-their-own-data-.patch | 104 + ...bsan-unsigned-integer-overflow-error.patch | 29 + ...tream-header-filter-s-per-header-siz.patch | 91 + ...-CPU-limit-check-interval-in-contain.patch | 40 + ...vecot_config_version-0.0.0-is-the-sa.patch | 92 + ...per-header-size-in-index_mail_get_he.patch | 42 + ...dr-value-cumulatively-against-header.patch | 123 + ...config_version_find-from-src-config-.patch | 123 + ...ter-Move-version_-to-lib-version.-ch.patch | 591 ++++ ...ce-struct-message_part_data_limits-a.patch | 323 ++ ...econf-dF-Add-dovecot_storage_version.patch | 63 + ...otal-address-count-per-message-to-10.patch | 71 + ...otal-Content-Language-tag-count-per-.patch | 99 + ...otal-MIME-parameter-count-per-messag.patch | 130 + patches/CVE-2025-59028.patch | 40 + patches/CVE-2025-59031.patch | 137 + patches/CVE-2025-59032.patch | 30 + patches/CVE-2026-24031-27860-1.patch | 39 + patches/CVE-2026-24031-27860-2.patch | 113 + patches/CVE-2026-24031-27860-3.patch | 32 + patches/CVE-2026-24031-27860-4.patch | 64 + patches/CVE-2026-24031-27860-5.patch | 224 ++ patches/CVE-2026-24031-27860-6.patch | 36 + patches/CVE-2026-24031-27860-7.patch | 37 + patches/CVE-2026-24031-27860-8.patch | 60 + patches/CVE-2026-27851.patch | 46 + patches/CVE-2026-27855-1.patch | 21 + patches/CVE-2026-27855-2.patch | 92 + patches/CVE-2026-27855-3.patch | 44 + patches/CVE-2026-27855-4.patch | 30 + patches/CVE-2026-27856-1.patch | 54 + patches/CVE-2026-27856-2.patch | 54 + patches/CVE-2026-27856-3.patch | 26 + patches/CVE-2026-27857-1.patch | 27 + patches/CVE-2026-27857-2.patch | 253 ++ patches/CVE-2026-27857-3.patch | 154 + patches/CVE-2026-27857-4.patch | 83 + patches/CVE-2026-27857-5.patch | 53 + patches/CVE-2026-27858.patch | 31 + patches/CVE-2026-27859.patch | 100 + patches/CVE-2026-33603.patch | 36 + patches/CVE-2026-40016.patch | 255 ++ patches/CVE-2026-40020-1.patch | 78 + patches/CVE-2026-40020-2.patch | 28 + patches/CVE-2026-40020-3.patch | 23 + patches/CVE-2026-42006.patch | 101 + patches/Use-_FORTIFY_SOURCE-level-3.patch | 58 + ...en-group-ACLs-are-used-but-user-s-ac.patch | 28 + ...rly_auth_oauth2_post_setting_defines.patch | 25 + ...CHE_KEY_USER_instead_of_per-database.patch | 124 + patches/bug1104549-gssapi-regression.patch | 21 + ...ug1106784_Fix-LDAP-SASL-auth-support.patch | 39 + ...is_set_but_iterate_fields_is_not_set.patch | 28 + patches/fit-32-bit-test-integers.patch | 61 + patches/fix-man-errors.patch | 72 + ...-header-filter-Fix-potential-assert-.patch | 84 + ...reate_common_Correctly_handle_errors.patch | 26 + patches/series | 212 ++ patches/skip-rfc-subdir.patch | 34 + po/nl.po | 102 + rules | 250 ++ salsa-ci.yml | 9 + source/format | 1 + source/lintian-overrides | 1 + source_dovecot.py | 38 + tests/control | 14 + tests/doveadm | 7 + tests/systemd | 22 + tests/testmails | 254 ++ tests/usage/00_setup | 57 + tests/usage/imap | 38 + tests/usage/pop3 | 31 + tests/usage/sieve | 8 + upstream/metadata | 5 + upstream/signing-key.asc | 25 + watch | 8 + 298 files changed, 25492 insertions(+) create mode 100755 autogen.sh create mode 100644 changelog create mode 100644 conf/conf.d/10-auth.conf create mode 100644 conf/conf.d/10-logging.conf create mode 100644 conf/conf.d/10-mail.conf create mode 100644 conf/conf.d/10-master.conf create mode 100644 conf/conf.d/10-metrics.conf create mode 100644 conf/conf.d/10-ssl.conf create mode 100644 conf/conf.d/15-lda.conf create mode 100644 conf/conf.d/15-mailboxes.conf create mode 100644 conf/conf.d/20-imap.conf create mode 100644 conf/conf.d/20-lmtp.conf create mode 100644 conf/conf.d/20-managesieve.conf create mode 100644 conf/conf.d/20-pop3.conf create mode 100644 conf/conf.d/20-submission.conf create mode 100644 conf/conf.d/30-dict-server.conf create mode 100644 conf/conf.d/90-acl.conf create mode 100644 conf/conf.d/90-fts-flatcurve.conf create mode 100644 conf/conf.d/90-fts.conf create mode 100644 conf/conf.d/90-quota.conf create mode 100644 conf/conf.d/90-sieve-extprograms.conf create mode 100644 conf/conf.d/90-sieve.conf create mode 100644 conf/conf.d/99-local.conf create mode 100644 conf/conf.d/auth-deny.conf.ext create mode 100644 conf/conf.d/auth-ldap.conf.ext create mode 100644 conf/conf.d/auth-master.conf.ext create mode 100644 conf/conf.d/auth-oauth2.conf.ext create mode 100644 conf/conf.d/auth-passwdfile.conf.ext create mode 100644 conf/conf.d/auth-sql.conf.ext create mode 100644 conf/conf.d/auth-static.conf.ext create mode 100644 conf/conf.d/auth-system.conf.ext create mode 100644 conf/dovecot.conf create mode 100644 conf/local.conf create mode 100644 control create mode 100644 copyright create mode 100644 dh.pem create mode 100644 dovecot-core.NEWS create mode 100644 dovecot-core.README.Debian create mode 100644 dovecot-core.bug-control create mode 100644 dovecot-core.bug-script create mode 100644 dovecot-core.dirs create mode 100644 dovecot-core.docs create mode 100644 dovecot-core.dovecot.default create mode 100644 dovecot-core.dovecot.init create mode 100644 dovecot-core.dovecot.pam create mode 100644 dovecot-core.install create mode 100644 dovecot-core.lintian-overrides create mode 100644 dovecot-core.manpages create mode 100644 dovecot-core.postinst create mode 100644 dovecot-core.postrm create mode 100644 dovecot-core.triggers create mode 100644 dovecot-dev.README.Debian create mode 100644 dovecot-dev.lintian-overrides create mode 100644 dovecot-flatcurve.links create mode 100644 dovecot-flatcurve.postinst create mode 100644 dovecot-flatcurve.postrm create mode 100644 dovecot-flatcurve.prerm create mode 100644 dovecot-flatcurve.triggers create mode 100644 dovecot-gssapi.links create mode 100644 dovecot-imapd.links create mode 100644 dovecot-imapd.lintian-overrides create mode 100644 dovecot-imapd.postinst create mode 100644 dovecot-imapd.postrm create mode 100644 dovecot-imapd.prerm create mode 100644 dovecot-imapd.triggers create mode 100644 dovecot-imapd.ufw.profile create mode 100644 dovecot-ldap.links create mode 100644 dovecot-ldap.postinst create mode 100644 dovecot-ldap.postrm create mode 100644 dovecot-ldap.triggers create mode 100644 dovecot-lmtpd.links create mode 100644 dovecot-lmtpd.postinst create mode 100644 dovecot-lmtpd.postrm create mode 100644 dovecot-lmtpd.prerm create mode 100644 dovecot-lmtpd.triggers create mode 100644 dovecot-managesieved.README.Debian create mode 100644 dovecot-managesieved.install create mode 100644 dovecot-managesieved.links create mode 100644 dovecot-managesieved.lintian-overrides create mode 100644 dovecot-managesieved.postinst create mode 100644 dovecot-managesieved.postrm create mode 100644 dovecot-managesieved.prerm create mode 100644 dovecot-managesieved.triggers create mode 100644 dovecot-mysql.links create mode 100644 dovecot-mysql.triggers create mode 100644 dovecot-openssl.cnf create mode 100644 dovecot-pgsql.links create mode 100644 dovecot-pgsql.triggers create mode 100644 dovecot-pop3d.links create mode 100644 dovecot-pop3d.postinst create mode 100644 dovecot-pop3d.postrm create mode 100644 dovecot-pop3d.prerm create mode 100644 dovecot-pop3d.triggers create mode 100644 dovecot-pop3d.ufw.profile create mode 100644 dovecot-sieve.links create mode 100644 dovecot-sieve.lintian-overrides create mode 100644 dovecot-sieve.postinst create mode 100644 dovecot-sieve.postrm create mode 100644 dovecot-sieve.triggers create mode 100644 dovecot-solr.README.Debian create mode 100644 dovecot-solr.install create mode 100644 dovecot-solr.links create mode 100644 dovecot-solr.triggers create mode 100644 dovecot-sqlite.links create mode 100644 dovecot-sqlite.triggers create mode 100644 dovecot-submissiond.postinst create mode 100644 dovecot-submissiond.postrm create mode 100644 dovecot-submissiond.prerm create mode 100644 dovecot-submissiond.triggers create mode 100644 gbp.conf create mode 100644 maildirmake.dovecot create mode 100644 maildirmake.dovecot.1 create mode 100644 patches/0001-auth-db-oauth2-Reduce-nesting-in-token_in_scope-via-.patch create mode 100644 patches/0001-commin-Rename-LOGIN_PROXY_FAILURE_TYPE_AUTH-to-LOGIN.patch create mode 100644 patches/0001-dsync-Avoid-potentially-excessive-data-stack-growth-.patch create mode 100644 patches/0001-global-Fix-spelling.patch create mode 100644 patches/0001-imap-Extract-side-channel-ostream-creation-into-help.patch create mode 100644 patches/0001-imap-Restrict-COMPRESS-to-DEFLATE-only.patch create mode 100644 patches/0001-imap-hibernate-Fix-out-of-bounds-read-when-parsing-D.patch create mode 100644 patches/0001-imap-login-Add-comments-to-internal-ID-command-param.patch create mode 100644 patches/0001-imap-login-Fix-excessive-memory-growth-with-pre-logi.patch create mode 100644 patches/0001-lda-Fix-using-USER-environment-if-d-hasn-t-been-spec.patch create mode 100644 patches/0001-lib-Add-XXH64-hash-implementation.patch create mode 100644 patches/0001-lib-Add-str_equals_timing_safe.patch create mode 100644 patches/0001-lib-compression-Don-t-calculate-crc32-if-using-zlib-.patch create mode 100644 patches/0001-lib-compression-istream-lz4-Ensure-uncompressed-chun.patch create mode 100644 patches/0001-lib-compression-istream-lz4-Try-again-if-no-data-was.patch create mode 100644 patches/0001-lib-compression-istream-zlib-Use-container_of-macro.patch create mode 100644 patches/0001-lib-compression-wrap-_read-in-for-loop-reindent-only.patch create mode 100644 patches/0001-lib-i_close_fd-Document-that-it-preserves-errno.patch create mode 100644 patches/0001-lib-imap-imap-match-Fix-excessive-CPU-usage-caused-b.patch create mode 100644 patches/0001-lib-imap-urlauth-Fix-leaking-uninitialized-memory-in.patch create mode 100644 patches/0001-lib-lda-mail-deliver-Autocreate-delivery-mailbox-via.patch create mode 100644 patches/0001-lib-mail-Reset-charset-translation-buffer-between-MI.patch create mode 100644 patches/0001-lib-mail-o_stream_dot_sendv-Use-I_MIN.patch create mode 100644 patches/0001-lib-mail-ostream-dot-Optimize-stream-writing.patch create mode 100644 patches/0001-lib-managesieve-managesieve-parser-Fix-handling-of-l.patch create mode 100644 patches/0001-lib-sieve-Add-per-user-sieve-rusage-file-for-cumulat.patch create mode 100644 patches/0001-lib-sieve-actions-Split-mailbox-creation-into-sieve_.patch create mode 100644 patches/0001-lib-sieve-sieve-binary-code-Fix-single-byte-oob-stac.patch create mode 100644 patches/0001-lib-sieve-sieve-storage-Add-is_personal-bit.patch create mode 100644 patches/0001-lib-sieve-sieve-storage-Fix-panic-caused-by-flawed-d.patch create mode 100644 patches/0001-lib-sieve-storage-file-Refuse-symlinks-escaping-pers.patch create mode 100644 patches/0001-lib-sieve-util-edit-mail-Fix-writing-to-freed-memory.patch create mode 100644 patches/0001-lib-sql-Add-sql_result_new_error-helper.patch create mode 100644 patches/0001-lib-sql-Use-strchr-based-while-loop-to-fill-template.patch create mode 100644 patches/0001-lib-sql-driver-sqlite-Use-sqlite3_snprintf-to-quote-.patch create mode 100644 patches/0001-lib-sql-sql-api-Implement-async-calls-for-drivers-th.patch create mode 100644 patches/0001-lib-storage-Allow-NULL-path-in-mailbox_list_mkdir_ro.patch create mode 100644 patches/0001-lib-storage-thread-Avoid-excessive-data-stack-growth.patch create mode 100644 patches/0001-lib-test-Add-test_assert_memcmp.patch create mode 100644 patches/0001-lib-test-Require-both-lengths-in-test_assert_memcmp.patch create mode 100644 patches/0001-lib-xxh64-fix-byte-ordering-bug-on-big-endian-system.patch create mode 100644 patches/0001-login-Add-LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED.patch create mode 100644 patches/0001-login-common-Fix-crash-when-XCLIENT-FORWARD-base64-c.patch create mode 100644 patches/0001-login-common-client-common-Add-client_disconnect-vfu.patch create mode 100644 patches/0001-login-common-login-Add-proxy_dest_connection_limit-e.patch create mode 100644 patches/0001-managesieve-login-Adjust-to-core-s-login_proxy_failu.patch create mode 100644 patches/0001-managesieve-login-Support-LOGIN_PROXY_FAILURE_TYPE_A.patch create mode 100644 patches/0001-submission-Fix-settings-leak-on-error-paths-in-clien.patch create mode 100644 patches/0001-trash-Use-mailbox-event-in-trash_try_mailbox-for-set.patch create mode 100644 patches/0002-auth-Fix-prefixing-forward_fields-without-a-value-fr.patch create mode 100644 patches/0002-auth-db-oauth2-Fix-scope-check-to-require-all-config.patch create mode 100644 patches/0002-doveadm-Avoid-leaking-doveadm_password-or-doveadm_ap.patch create mode 100644 patches/0002-doveadm-compress-connect-Don-t-hang-when-server-reje.patch create mode 100644 patches/0002-dsync-Fix-escaping-mail-or-attribute-value-that-begi.patch create mode 100644 patches/0002-imap-Recreate-multiplex-ostream-side-channel-after-u.patch create mode 100644 patches/0002-imap-Stream-BODYSTRUCTURE-to-client-without-ostream-.patch create mode 100644 patches/0002-imap-hibernate-Use-imap-parser-API-for-parsing-DONE-.patch create mode 100644 patches/0002-lda-Default-mail_home-HOME-environment-if-not-using-.patch create mode 100644 patches/0002-lib-Replace-str_hash-strcase_hash-with-xxh64.patch create mode 100644 patches/0002-lib-compression-Iterate-instead-of-recursing-on-zero.patch create mode 100644 patches/0002-lib-mail-o_stream_dot_sendv-Fix-boundary-off-by-one.patch create mode 100644 patches/0002-lib-path-util-Add-t_openat_safe-for-jailed-openat-re.patch create mode 100644 patches/0002-lib-sieve-Drop-binary-header-resource-usage-tracking.patch create mode 100644 patches/0002-lib-sieve-actions-Create-fileinto-mailbox-without-th.patch create mode 100644 patches/0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch create mode 100644 patches/0002-lib-sieve-util-test-edit-mail-Adjust-to-changes-in-d.patch create mode 100644 patches/0002-lib-sql-Extract-sql_query_callback_delayed-helper.patch create mode 100644 patches/0002-lib-storage-Remove-MAILBOX_FLAG_AUTO_CREATE-and-MAIL.patch create mode 100644 patches/0002-lib-storage-Simplify-mailbox_mkdir-using-mailbox_lis.patch create mode 100644 patches/0002-lib-storage-thread-Limit-References-header-msgid-cou.patch create mode 100644 patches/0002-login-common-Avoid-array_front-panic-on-empty-forwar.patch create mode 100644 patches/0002-managesieve-login-client_skip_line-Discard-data-when.patch create mode 100644 patches/0002-submission-login-client-Fix-panic-occurring-at-mail_.patch create mode 100644 patches/0003-auth-db-oauth2-Add-oauth2_audience-setting-deprecate.patch create mode 100644 patches/0003-doveadm-compress-Delay-failing-if-used-COMPRESS-algo.patch create mode 100644 patches/0003-imap-Stream-ENVELOPE-to-client-without-ostream-memor.patch create mode 100644 patches/0003-lib-Add-str_stable_hash.patch create mode 100644 patches/0003-lib-Add-t_openat_safe_dir.patch create mode 100644 patches/0003-lib-compression-Add-o_stream_deflate_reset_dict.patch create mode 100644 patches/0003-lib-compression-istream-zstd-Guard-against-no-progre.patch create mode 100644 patches/0003-lib-mail-o_stream_dot_sendv-Use-enumeration-to-defin.patch create mode 100644 patches/0003-lib-sieve-Rename-on-disk-header-rusage-fields-to-unu.patch create mode 100644 patches/0003-lib-sieve-edit_mail_headers_parse-Fix-info-leak-via-.patch create mode 100644 patches/0003-lib-smtp-smtp-server-Expose-smtp_server_connection_r.patch create mode 100644 patches/0003-lib-sql-Add-sql_commit_schedule_delayed-helper.patch create mode 100644 patches/0003-lib-storage-thread-Limit-ancestor-chain-traversal-de.patch create mode 100644 patches/0004-acl-dovecot-acl-list-is-never-supposed-to-be-a-symli.patch create mode 100644 patches/0004-imap-login-Add-dict_reset-side-channel-command-to-im.patch create mode 100644 patches/0004-lib-Key-str_hash-strcase_hash-with-a-random-seed-to-.patch create mode 100644 patches/0004-lib-compression-test-Add-zero-len-frame-test-for-all.patch create mode 100644 patches/0004-lib-mail-message-parser-from-parts-Enforce-50-MB-all.patch create mode 100644 patches/0004-lib-mail-o_stream_dot_sendv-Do-not-send-unguarded-.-.patch create mode 100644 patches/0004-lib-sieve-Switch-sieve_max_cpu_time-enforcement-to-t.patch create mode 100644 patches/0004-lib-var-expand-Change-escape-func-signature-to-retur.patch create mode 100644 patches/0004-submission-login-client-authenticate-Reply-421-4.7.0.patch create mode 100644 patches/0005-imap-Add-imap_compress_on_proxy-hidden-setting.patch create mode 100644 patches/0005-lib-Add-comments-about-memory-allocations-and-string.patch create mode 100644 patches/0005-lib-index-mail-index-strmap-Fix-OOB-read-from-trunca.patch create mode 100644 patches/0005-lib-mail-Add-count-field-to-struct-message_address_l.patch create mode 100644 patches/0005-lib-sieve-file-storage-Remove-orphan-compiled-binari.patch create mode 100644 patches/0005-lib-sql-Make-escape_string-return-int-with-error_r-f.patch create mode 100644 patches/0005-submission-login-submission-proxy-Recognize-421-4.7..patch create mode 100644 patches/0006-auth-passdb_sql-connect-before-expanding-query-varia.patch create mode 100644 patches/0006-imap-Reset-DEFLATE-dictionary-after-every-tagged-rep.patch create mode 100644 patches/0006-lib-index-Rename-crc32-variables-parameters-to-hash-.patch create mode 100644 patches/0006-lib-sieve-Log-a-warning-when-cumulative-CPU-exceeds-.patch create mode 100644 patches/0006-lib-storage-mbox-Explicitly-disable-the-header-block.patch create mode 100644 patches/0007-lib-index-Add-keyed-xxh64-strmap-format-v2-gated-by-.patch create mode 100644 patches/0007-lib-mail-istream-header-filter-Use-container_of-for-.patch create mode 100644 patches/0007-lib-sieve-Log-an-info-line-when-opening-a-CPU-disabl.patch create mode 100644 patches/0007-lib-sql-Run-sql-statement-queries-in-their-own-data-.patch create mode 100644 patches/0008-doveadm-Fix-ubsan-unsigned-integer-overflow-error.patch create mode 100644 patches/0008-lib-mail-Make-istream-header-filter-s-per-header-siz.patch create mode 100644 patches/0008-lib-sieve-Reduce-CPU-limit-check-interval-in-contain.patch create mode 100644 patches/0009-config-Assume-dovecot_config_version-0.0.0-is-the-sa.patch create mode 100644 patches/0009-lib-storage-Cap-per-header-size-in-index_mail_get_he.patch create mode 100644 patches/0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch create mode 100644 patches/0010-lib-master-Move-config_version_find-from-src-config-.patch create mode 100644 patches/0011-lib-lib-master-Move-version_-to-lib-version.-ch.patch create mode 100644 patches/0011-lib-mail-Introduce-struct-message_part_data_limits-a.patch create mode 100644 patches/0012-config-doveconf-dF-Add-dovecot_storage_version.patch create mode 100644 patches/0012-lib-mail-Limit-total-address-count-per-message-to-10.patch create mode 100644 patches/0013-lib-mail-Limit-total-Content-Language-tag-count-per-.patch create mode 100644 patches/0014-lib-mail-Limit-total-MIME-parameter-count-per-messag.patch create mode 100644 patches/CVE-2025-59028.patch create mode 100644 patches/CVE-2025-59031.patch create mode 100644 patches/CVE-2025-59032.patch create mode 100644 patches/CVE-2026-24031-27860-1.patch create mode 100644 patches/CVE-2026-24031-27860-2.patch create mode 100644 patches/CVE-2026-24031-27860-3.patch create mode 100644 patches/CVE-2026-24031-27860-4.patch create mode 100644 patches/CVE-2026-24031-27860-5.patch create mode 100644 patches/CVE-2026-24031-27860-6.patch create mode 100644 patches/CVE-2026-24031-27860-7.patch create mode 100644 patches/CVE-2026-24031-27860-8.patch create mode 100644 patches/CVE-2026-27851.patch create mode 100644 patches/CVE-2026-27855-1.patch create mode 100644 patches/CVE-2026-27855-2.patch create mode 100644 patches/CVE-2026-27855-3.patch create mode 100644 patches/CVE-2026-27855-4.patch create mode 100644 patches/CVE-2026-27856-1.patch create mode 100644 patches/CVE-2026-27856-2.patch create mode 100644 patches/CVE-2026-27856-3.patch create mode 100644 patches/CVE-2026-27857-1.patch create mode 100644 patches/CVE-2026-27857-2.patch create mode 100644 patches/CVE-2026-27857-3.patch create mode 100644 patches/CVE-2026-27857-4.patch create mode 100644 patches/CVE-2026-27857-5.patch create mode 100644 patches/CVE-2026-27858.patch create mode 100644 patches/CVE-2026-27859.patch create mode 100644 patches/CVE-2026-33603.patch create mode 100644 patches/CVE-2026-40016.patch create mode 100644 patches/CVE-2026-40020-1.patch create mode 100644 patches/CVE-2026-40020-2.patch create mode 100644 patches/CVE-2026-40020-3.patch create mode 100644 patches/CVE-2026-42006.patch create mode 100644 patches/Use-_FORTIFY_SOURCE-level-3.patch create mode 100644 patches/acl-Fix-crash-when-group-ACLs-are-used-but-user-s-ac.patch create mode 100644 patches/auth__Terminate_properly_auth_oauth2_post_setting_defines.patch create mode 100644 patches/auth__Use_AUTH_CACHE_KEY_USER_instead_of_per-database.patch create mode 100644 patches/bug1104549-gssapi-regression.patch create mode 100644 patches/bug1106784_Fix-LDAP-SASL-auth-support.patch create mode 100644 patches/bug1121000_dovecot-ldap_Crash_if_iterate_filter_is_set_but_iterate_fields_is_not_set.patch create mode 100644 patches/fit-32-bit-test-integers.patch create mode 100644 patches/fix-man-errors.patch create mode 100644 patches/lib-mail-istream-header-filter-Fix-potential-assert-.patch create mode 100644 patches/lib-sieve_sieve-script_c_sieve_script_create_common_Correctly_handle_errors.patch create mode 100644 patches/series create mode 100644 patches/skip-rfc-subdir.patch create mode 100644 po/nl.po create mode 100755 rules create mode 100644 salsa-ci.yml create mode 100644 source/format create mode 100644 source/lintian-overrides create mode 100644 source_dovecot.py create mode 100644 tests/control create mode 100755 tests/doveadm create mode 100755 tests/systemd create mode 100755 tests/testmails create mode 100755 tests/usage/00_setup create mode 100755 tests/usage/imap create mode 100755 tests/usage/pop3 create mode 100755 tests/usage/sieve create mode 100644 upstream/metadata create mode 100644 upstream/signing-key.asc create mode 100644 watch diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..ba3a3cb --- /dev/null +++ b/autogen.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +set -eu + +autoreconf -f -i -v -Wall +(cd pigeonhole && autoreconf -f -i -v -Wall) diff --git a/changelog b/changelog new file mode 100644 index 0000000..84b35de --- /dev/null +++ b/changelog @@ -0,0 +1,3143 @@ +dovecot (1:2.4.1+dfsg1-6+deb13u7) trixie-security; urgency=medium + + * Import upstream fixes for multiple security issues. (Closes: #1146018) + - CVE-2026-27852: DoS by sending mail with bad header + - CVE-2026-33263: submission-login: Panic when + mail_max_userip_connections is reached: Panic: epoll_ctl(del, 8) + failed: Bad file descriptor + - CVE-2026-33604: SMTP Smuggling via Missing Dot-Stuffing After Bare + Carriage Return + - CVE-2026-33605: managesieve-login: Pre-auth crash + - CVE-2026-33607: Dovecot IMAP LIST match_sub() Exponential + Backtracking — CPU Denial of Service + - CVE-2026-40013: pigeonhole: Stack Buffer Underflow in Pigeonhole + ManageSieve CHECKSCRIPT/PUTSCRIPT + - CVE-2026-40014: IMAP THREAD REFERENCES O(N²) CPU DoS via Crafted + References Header (index-thread-links.c) + - CVE-2026-40015: imap-hibernate can be crashed + - CVE-2026-40017: IMAP THREAD O(M³) CPU DoS via CRC32 Hash Collision + in strmap (mail-index-strmap.c / hash2.c) + - CVE-2026-40018: MySQL multi-byte escaping wrong + - CVE-2026-40204: acl: lda_mailbox_autocreate can bypass acl + restrictions + - CVE-2026-42007: Sieve editheader RCE + - CVE-2026-42391: imap: Pre-login memory/CPU growth with ID command + - CVE-2026-42394: sieve: symlink traversal flaw could result in + arbitrary file disclosure + - CVE-2026-52681: Sieve resource usage tracking lost when active + script changes + - CVE-2026-52687: IMAP: COMPRESS ZSTD can cause excessive memory usage + - CVE-2026-73209: imap-login crash: Self-recursion on zero-output + decompress chunks + - CVE-2026-33606: dsync: Mail content can cause dsync protocol + injection + - CVE-2026-40203: IMAP Compression Can Reveal Whether a Small Synced + Email Body Matches Sender-Chosen Text + - CVE-2026-42008: XCLIENT FORWARD= bare token not namespaced, allows + nopassword injection via trusted proxy + - CVE-2026-42392: imap-urlauth leaks memory into user-visible error + messages + - CVE-2026-42393: doveadm_password or api key length can still be + leaked with timing comparisons + - CVE-2026-42395: Single NUL-Byte XCLIENT FORWARD Payload Crashes + - CVE-2026-40205: OAuth2 passdb scope enforcement bypass via OR + semantics in remote validation path + - CVE-2026-73208: auth: db-oauth2: aud claim used as fallback for + missing scope claim + * lib-mail: istream-header-filter - Fix potential assert-crash + (Closes: #1144639) + * CI: Disable test-build-twice job, which is known to fail on trixie + + -- Noah Meyerhans Wed, 16 Sep 2026 15:06:35 -0400 + +dovecot (1:2.4.1+dfsg1-6+deb13u6) trixie-security; urgency=medium + + * Security update (Closes: #1136444) + * [76ceed4] CVE-2026-27851: lib-var-expand: Reset safe state when + transfer is unset + * [4af6fb3] CVE-2026-40016: lib-sieve: Enforce CPU time limit within + :contains and :matches matcher loops + * [366ef61] CVE-2026-33603: login-common: Only accept base64 in sasl + * [26bd41e] CVE-2026-40020: IMAP folders can be shared-spammed to + everyone. + * [b6f5bac] CVE-2026-42006: imap-login: Excessive memory usage DoS + + -- Noah Meyerhans Mon, 18 May 2026 16:03:51 -0400 + +dovecot (1:2.4.1+dfsg1-6+deb13u5) trixie; urgency=medium + + * [b357180] autopkgtests: Add managesieved authentication test + * [c9d69a1] Fix memory leak in CVE-2026-27857 fix + + -- Noah Meyerhans Wed, 06 May 2026 15:18:43 -0400 + +dovecot (1:2.4.1+dfsg1-6+deb13u4) trixie-security; urgency=medium + + * [bc29057] CVE-2025-59028: auth: Don't disconnect auth client when + invalid base64 SASL input is received + * [fee7a9a] CVE-2025-59031: stop shipping the decode2text shell script + * [9a4442e] CVE-2025-59032: managesieve-login: Fix crash when command + didn't finish on the first call + * [2711b3e] CVE-2026-24031, CVE-2026-27860: auth: fix ldap and sql + injection + * [d30f1c3] CVE-2026-27855: fix OTP authentication reply vulnerability + * [e1b0ff7] CVE-2026-27856: doveadm: fix timing oracle attack + * [b8a69bf] CVE-2026-27857: fix resource exhaustion DoS in NOOP command + parsing + * [85dd068] CVE-2026-27858: fix pre-authentication managesieve memory + consumption issue + * [880e332] CVE-2026-27859: fix uncontrolled resource allocation when + delivering specially crafted email messages + + -- Noah Meyerhans Tue, 31 Mar 2026 15:07:17 -0400 + +dovecot (1:2.4.1+dfsg1-6+deb13u3) trixie; urgency=medium + + * [1186296] import upstream fix for possible crash in ldap userdb + (Closes: #1121000) + * [14c13a5] backport upstream fix for crash in trash plugin (Closes: #1127029) + * [95bf3b3] Backport fix for segfault when group ACLs are present but the + user has no groups (Closes: #1129952) + + -- Noah Meyerhans Fri, 06 Mar 2026 09:36:28 -0500 + +dovecot (1:2.4.1+dfsg1-6+deb13u2) trixie; urgency=medium + + * [6ac2883] Clean up a few typos in default/example config (Closes: #1112667) + * [7feb544] Ensure default lmtpd auth_username_format matches the global value + (Closes: #1111469) + * [216ec20] import upstream patch for improperly terminated auth_oauth2_post_setting_defines + (Closes: #1116328) + * [46eab61] lib-sieve/sieve-script.c: sieve_script_create_common: Correctly handle errors. + (Closes: #1116070) + + -- Noah Meyerhans Thu, 23 Oct 2025 10:01:33 -0400 + +dovecot (1:2.4.1+dfsg1-6+deb13u1) trixie-security; urgency=high + + * Import upstream fix for an issue with authentication cache management that + could result in users being logged in as the wrong user in certain + configurations. (CVE-2025-30189) (Closes: #1115964) + + -- Noah Meyerhans Wed, 24 Sep 2025 09:14:50 -0400 + +dovecot (1:2.4.1+dfsg1-6) unstable; urgency=medium + + * [8c6ba88] Fix LDAP SASL auth support (Closes: #1106784) + * [fac9131] Avoid -fstack-clash-protection on hppa architecture + (Closes: #1107609) + * [071beb5] d/control: update Uploaders to reflect the current reality + + -- Noah Meyerhans Wed, 18 Jun 2025 10:01:58 -0400 + +dovecot (1:2.4.1+dfsg1-5) unstable; urgency=medium + + * [e6e5ef7] Fix typo in conf.d/auth-passwdfile.conf.ext (Closes: #1106072) + * [2a34ed0] Import upstream fix for dovecot-lda data loss (Closes: #1106485) + * [6c287ea] Fix typo in doveadm-fetch(1) (Closes: #1106533) + * [11f8b62] Fix invalid metadata in debian/patches/skip-rfc-subdir.patch + + -- Noah Meyerhans Wed, 28 May 2025 15:45:52 -0400 + +dovecot (1:2.4.1+dfsg1-4) unstable; urgency=medium + + * [235de56] managesieved: lintian: suppress library-not-linked-against-libc + * [74e7dcd] Ensure we use a 32-bit time_t on i386 + * [461bc68] dovecot-core: postinst: fix purge of obsolete ucf conf files + (Closes: #1105003) + + -- Noah Meyerhans Mon, 12 May 2025 20:36:26 -0400 + +dovecot (1:2.4.1+dfsg1-3) unstable; urgency=medium + + * [bdf895a] upstream fix for GSSAPI authentication regression + (Closes: #1104549) + * [8f15017] d/copyright: Correct canonical reference to GNU licenses + * [ad8f661] dovecot-flatcurve: correct missing dependency on ucf + * [131567a] Fix miscellaneous groff errors in upstream manpages + * [a4a1404] Correct lintian warning debian-news-entry-has-unknown-version + + -- Noah Meyerhans Fri, 02 May 2025 11:20:05 -0400 + +dovecot (1:2.4.1+dfsg1-2) unstable; urgency=medium + + * [7c1b5b9] dovecot-ldap: postinst: correct conffile list (Closes: #1104047) + * [73a254a] config: set mail_home and add mbox deprecation note + + -- Noah Meyerhans Mon, 28 Apr 2025 15:53:54 -0400 + +dovecot (1:2.4.1+dfsg1-1) unstable; urgency=medium + + * Release 1:2.4.1+dfsg1-1 to unstable + + -- Noah Meyerhans Thu, 24 Apr 2025 13:06:17 -0400 + +dovecot (1:2.4.1+dfsg1-1~exp2) experimental; urgency=medium + + * [e30a611] flatcurve: Set breaks+replaces relationship on dovecot-fts-flatcurve + (Closes: #1102937) + + -- Noah Meyerhans Fri, 18 Apr 2025 15:34:42 -0400 + +dovecot (1:2.4.1+dfsg1-1~exp1) experimental; urgency=medium + + * [18c4170] d/watch: handle suffixes in upstream release filenames + * [340250d] New upstream version 2.4.1+dfsg1 + * [2fcd382] Ensure we don't try to download files from the internet during build + * [4497016] refresh patches + * [af108c1] Remove obsolete patches + * [0ed2624] Fix 32-bit integer rollover in tests + * [528f075] d/rules: remove autogenerated sources on clean + + -- Noah Meyerhans Sun, 30 Mar 2025 11:48:57 -0400 + +dovecot (1:2.4.0+dfsg1-1~exp6) experimental; urgency=medium + + * [827f8b7] fix and re-enable 32-bit builds + + -- Noah Meyerhans Mon, 24 Mar 2025 15:16:57 -0400 + +dovecot (1:2.4.0+dfsg1-1~exp5) experimental; urgency=medium + + * [cea2f60] d/rules: remove additional files on clean + * [24e625b] ci: extend the timeout for the test-build-twice job + * [1df7d66] Fix ABI identification and add safety checking for it + + -- Noah Meyerhans Mon, 17 Mar 2025 09:56:33 -0400 + +dovecot (1:2.4.0+dfsg1-1~exp4) experimental; urgency=medium + + * [0806d3b] Move 90-sieve-extprograms back to dovecot-sieve where it belongs + (Closes: #1100146) + + -- Noah Meyerhans Thu, 13 Mar 2025 17:41:17 -0400 + +dovecot (1:2.4.0+dfsg1-1~exp3) experimental; urgency=medium + + * [4989934] Install a config file for flatcurve + * [baeb072] Add default lmtpd configuration + * [1d1d874] d/rules: drop some obsolete configure options + * [6bc7f0b] pigeonhole: enable ldap pluggin support + * [1998623] Enable experimental support for SMTPUTF8 and UTF8=ACCEPT + * [22abd27] enable cdb support + * [369d4a4] ci: disable i386 builds + * [e6ff65b] update autopkgtests + * [56dd0d6] debian/dovecot-core.NEWS: Document configuration changes + * [3d12c1e] d/rules: Update ABI detection + + -- Noah Meyerhans Tue, 11 Mar 2025 11:31:12 -0400 + +dovecot (1:2.4.0+dfsg1-1~exp2) experimental; urgency=medium + + * [ab636e4] Correct ucf handling of /etc/dovecot/dovecot.conf + * [ea5102c] Stop building for 32-bit architectures + * [88fecb4] fix shell error in dovecot-managesieved.postinst + * [68875fa] Fix some groff issues in manpages + * [b406a2f] drop stale lintian overrides + * [34e4a3b] lintian: ignore circular dependency between -core and -sieve + * [ee13475] d/copyright: Update or remove some attributions + + -- Noah Meyerhans Wed, 12 Feb 2025 16:39:17 -0500 + +dovecot (1:2.4.0+dfsg1-1~exp1) experimental; urgency=medium + + [ Michael Tokarev ] + * Update to 2.4.0 + + [ Noah Meyerhans ] + * Update default configuration from upstream example config bundle at + https://github.com/dovecot/tools/blob/main/dovecot-2.4.0-example-config.tar.gz + * Remove obsolete config files on upgrade if unmodified + * Update /usr/share/dovecot/protocols.d/* for 2.4 + + -- Noah Meyerhans Tue, 11 Feb 2025 12:03:31 -0500 + +dovecot (1:2.3.21.1+dfsg1-1) unstable; urgency=medium + + [ Noah Meyerhans ] + * [452a10b] Move systemd unit files to /usr (Closes: #1071915) + + [ Niels Thykier ] + * [a9caf51] Avoid unnecessary implicit requirement for `(fake)root` + + [ Christian Göttsche ] + * [8c253d1] salsa-ci: enable build_twice job + * [47122cd] Bump to standards version 4.7.0 (no further changes) + * [4062094] Replace obsolete build-dependency pkg-config with pkgconf + * [f1221b8] Split overlong line in changelog + * [dd876aa] Annotate Debian patches + * [590287e] Fix typos in changelog + + [ Noah Meyerhans ] + * [a212eb8] New upstream version 2.3.21.1+dfsg1 + - Fix CVE-2024-23184 (Closes: #1078876) + - Fix CVE-2024-23185 (Closes: #1078877) + + -- Noah Meyerhans Sat, 17 Aug 2024 13:26:24 -0400 + +dovecot (1:2.3.21+dfsg1-3) unstable; urgency=medium + + * [883dc1a] Add libtirpc-dev to build-depends (Closes: #1065213) + + -- Noah Meyerhans Sat, 09 Mar 2024 22:31:22 -0800 + +dovecot (1:2.3.21+dfsg1-2) unstable; urgency=medium + + [ Christian Göttsche ] + * [a2fbc2f] split-protocols.patch: patch all-settings.c to successfully build + twice (Closes: #1044797) + + [ Noah Meyerhans ] + * [70e4426] Drop arm64 from libunwind builddep arch list + + -- Noah Meyerhans Mon, 30 Oct 2023 13:40:35 -0700 + +dovecot (1:2.3.21+dfsg1-1) unstable; urgency=medium + + [ Noah Meyerhans ] + * [753b4fe] Don't build the unmaintained lucene fts plugin (Closes: #1040884) + * [5597486] New upstream version 2.3.21+dfsg1 + + [ Christian Göttsche ] + * [b8017f1] Cleanup temporary build files + * [35e1afe] Silence prototype conflicts + * [8dda8b9] Update Lintian overrides + * [6bae82f] Bump to standards version 4.6.2 (no further changes) + * [1f973d2] Mark hurd patch forwarded + + -- Noah Meyerhans Sat, 14 Oct 2023 08:52:10 -0700 + +dovecot (1:2.3.20+dfsg1-1) unstable; urgency=medium + + [ Christian Göttsche ] + * [fb2a5b7] d/rules: enable stack clash protection + * [2666970] d/patches: bump _FORTIFY_SOURCE to level 3 + + [ Noah Meyerhans ] + * [eab5171] New upstream version 2.3.20+dfsg1 + * [d6135a4] Drop dependency on obsolete lsb-base package + + -- Noah Meyerhans Sun, 25 Jun 2023 16:17:56 -0700 + +dovecot (1:2.3.19.1+dfsg1-2.1) unstable; urgency=medium + + * Non-maintainer upload. + * [b02ebc9] Don't use deprecated crypt module. + (closes: #1028513) + + -- Bas Couwenberg Fri, 20 Jan 2023 07:01:26 +0100 + +dovecot (1:2.3.19.1+dfsg1-2) unstable; urgency=medium + + [ Christian Göttsche ] + * [281fb2c] d/patches: cherry-pick fix for CVE-2022-30550 (Closes: #1016351) + * [9c58e71] d/patches: fix uninitialized read in doveadm-oldstats + * [a76a24d] d/control: bump to standards version 4.6.1 (no further changes) + * [4aaaa8b] Update Lintian overrides + + -- Noah Meyerhans Fri, 29 Jul 2022 19:58:28 -0700 + +dovecot (1:2.3.19.1+dfsg1-1) unstable; urgency=medium + + [ Christian Göttsche ] + * [e40f93f] d/patches: avoid usage of PATH_MAX not available on hurd + * [19e00cd] d/rules: enable backtrace generation + * [5bf1c43] d/patches: debug flaky unit test + + [ Noah Meyerhans ] + * [b73422f] New upstream version 2.3.19.1+dfsg1 + * [c88bfc0] Update changelog for 1:2.3.19.1+dfsg1-1 release + * [ca59548] Update lintian overrides + * [d6406c2] d/copyright: update declarations for current maintainers + + -- Noah Meyerhans Wed, 22 Jun 2022 09:27:01 -0700 + +dovecot (1:2.3.19+dfsg1-1) unstable; urgency=medium + + [ Christian Göttsche ] + * [0d29e45] d/rules: enable LTO via DEB_BUILD_MAINT_OPTIONS instead of custom flags + * [560cceb] d/source/lintian-overrides: update very-long-line-length-in-source-file overrides + * [b99d09e] d/copyright: update years + * [9ee8271] d/dovecot-core.prerm: drop as superseded by debhelper + * [907f85c] d/maintscripts: update + * [2b38240] d/dovecot-core.postinst: drop support for version skips + * [dcb76d1] d/dovecot-core.postinst: only link certs if existent (Closes: #1009872) + * [d223bbd] d/patches: add patch to support openssl 3.0 (Closes: #996273) + + [ Noah Meyerhans ] + * [9f3175e] New upstream version 2.3.19+dfsg1 + + -- Noah Meyerhans Sun, 05 Jun 2022 18:29:18 +0000 + +dovecot (1:2.3.18+dfsg1-1) unstable; urgency=medium + + [ Noah Meyerhans ] + * [36966c8] New upstream version 2.3.18+dfsg1 + * [042bda4] Refresh patches for 1:2.3.18+dfsg1-1 + + -- "Noah Meyerhans" Thu, 10 Feb 2022 20:05:50 +0000 + +dovecot (1:2.3.17.1+dfsg1-1) unstable; urgency=medium + + [ Christian Göttsche ] + * [40b0010] New upstream version 2.3.17+dfsg1 + * [3c377e0] New upstream version 2.3.17.1+dfsg1 + * [e2f1ce2] d/patches: rebase and drop upstream applied ones + * [533b7ad] d/control: bump to standards version 4.6.0 (no further changes) + * [02ed6cf] debian: reduce Lintian issues + * [bb3ae48] d/salsa-ci.yml: skip cross build and do not fail on Lintian + warnings + * [bcda7e4] d/control: build against Lua 5.4 + * [9eed0dd] d/control: enable libunwind support on available archs + * [1990699] d/patches: cherry-pick memory leak commit + * [426df46] d/patches: cherry-pick imapsieve fix + * [e3d0747] d/patches: add patch for LTO by avoiding unaligned access + (Closes: #997513) + + -- Noah Meyerhans Tue, 14 Dec 2021 09:24:23 -0800 + +dovecot (1:2.3.16+dfsg1-3) unstable; urgency=medium + + * [7b858b6] Fix FTBFS on mips(64)el. Stacktrace generation on these + architectures requires -funwind-tables, as with 32-bit arm. + + -- Noah Meyerhans Thu, 16 Sep 2021 08:41:27 -0700 + +dovecot (1:2.3.16+dfsg1-2) unstable; urgency=medium + + [ Christian Göttsche ] + * [e1e9ece] d/patches: rework backtrace test patch + * [be404bf] d/patches: add big-endian patch + + -- Noah Meyerhans Fri, 10 Sep 2021 16:10:50 -0700 + +dovecot (1:2.3.16+dfsg1-1) unstable; urgency=medium + + [ Christian Göttsche ] + * [ff4a227] New upstream version 2.3.14+dfsg1 + * [963fa3b] New upstream version 2.3.15+dfsg1 (Closes: #991323, #983510) + * [5e0c898] d/watch: adjust dversionmangle for dfsg suffix + * [9ffb0f5] d/patches: update + * [850e1d6] New upstream version 2.3.16+dfsg1 + * [7140b87] d/patches: rebase patches + * [fb1b77e] d/rules: enable LTO + * [ce7055d] d/control: add libsystemd-dev dependency + * [db93263] d/copyright: drop unused section + * [aeec1e8] d/rules: update how to set systemdsystemunitdir + * [ebe9709] d/patches: resolve compiler warnings + * [19b2bb0] d/changelog: bump to 1:2.3.16+dfsg1-1 + * [58a4078] d/patches: update 32bit warnings patch + + [ Noah Meyerhans ] + * [f217c2e] Fix indexer crash + * [b075317] Import upstream patch for indexer crash on client disconnect + * [36e8740] drop debian/dovecot-core.maintscript + + -- Noah Meyerhans Thu, 02 Sep 2021 13:22:16 -0700 + +dovecot (1:2.3.13+dfsg1-2) unstable; urgency=high + + * Import upstream fixes for security issues (Closes: #990566): + - CVE-2021-29157: Path traversal issue allowing an attacker with + access to the local filesystem can trick OAuth2 authentication into + using an HS256 validation key from an attacker-controlled location + - CVE-2021-33515: Sensitive information could be redirected to an + attacker-controlled address because of a STARTTLS command injection + bug in the submission service + + -- Noah Meyerhans Tue, 20 Jul 2021 08:05:19 -0700 + +dovecot (1:2.3.13+dfsg1-1) unstable; urgency=medium + + [ Christian Göttsche ] + * [6829237] New upstream version 2.3.13 (Closes: #979363) + - CVE-2020-24386: IMAP hibernation allows accessing other peoples mail + - CVE-2020-25275: MIME parsing crashes with particular messages + + * [6d25736] Add libzstd-dev to build-dependencies (Closes: #969165) + * [5956798] Rebase patches + * [2cb63c3] Bump to standards version 4.5.1 (no further changes) + * [548bac5] Drop unmatched copyright src/lib-ntlm/* wildcard + * [6f33f3f] Ignore package-contains-documentation-outside-usr-share-doc + false-positives + * [dde9c94] Handle removed configuration file in postinst + + [ Pino Toscano ] + * [04a60e3] d/{control,rules}: disable apparmor support on !linux archs + (Closes: #951869) + + [ Helmut Grohne ] + * [e5f9fcb] d/patches: improve cross-compile support (Closes: #979370) + + -- Noah Meyerhans Mon, 25 Jan 2021 15:38:17 -0800 + +dovecot (1:2.3.11.3+dfsg1-2) unstable; urgency=medium + + [ Christian Göttsche ] + * [44770f6] Add patch for 32bit compiler warnings + * [053865a] Lintian: remove unused override + * [4ece2e1] Lintian: add forwarded header to Debian specific patches + * [67872b7] Lintian: ignore Debian only man page + * [d30bd7e] Lintian: tag manpage-without-executable got renamed to + spare-manual-page + * [3bdf952] Limit libcap-dev build-dependency to linux-any + * [28f6425] Drop acute accent in man page + * [8c15850] Add patch allowing GSSAPI containing NULL + + -- Noah Meyerhans Wed, 19 Aug 2020 12:06:07 -0700 + +dovecot (1:2.3.11.3+dfsg1-1) unstable; urgency=high + + * New upstream release fixes security issues (Closes: #968302) + - CVE-2020-12100 - Receiving mail with deeply nested MIME parts leads to + resource exhaustion as Dovecot attempts to parse it. + - CVE-2020-12673 - Dovecot's NTLM implementation does not correctly check + message buffer size, which leads to reading past allocation which can + lead to crash. + - CVE-2020-12674 - Dovecot's RPA mechanism implementation accepts + zero-length message, which leads to assert-crash later on. + * Add libcap-dev to build-dependencies to support dropping linux + capabilities. + + -- Noah Meyerhans Thu, 13 Aug 2020 16:21:24 -0700 + +dovecot (1:2.3.10.1+dfsg1-2) unstable; urgency=medium + + * Support sd_notify with systemd (Closes: #951722) + * Add necessary CFLAGS and LDFLAGS settings to ensure functional backtrace + generation. (Closes: #962630) + * Suppress additional library-not-linked-against-libc lintian warnings some + plugins as false-positives, observed on armel systems + + [ Andreas Hasenack ] + * d/t/control, d/t/testmails: cherry-pick updated autopkgtests from + Ubuntu's 1:2.2.35-2ubuntu1: + - d/t/testmails: dropped the hardcoded "Ubuntu" name from the banner + text and made it distribution agnostic + - d/t/control: added lsb-release to test dependencies, used to get the + distribution name + + -- Noah Meyerhans Tue, 16 Jun 2020 08:29:02 -0700 + +dovecot (1:2.3.10.1+dfsg1-1) unstable; urgency=medium + + * New upstream release addresses multiple security issues + - CVE-2020-10957 + - CVE-2020-10958 + - CVE-2020-10967 + (Closes: #960963, #930919, #928492) + * Refresh patches + * Strip non-DFSG-compliant docs from .orig archives + * Incorporate a number of improvements to debian/ metadata contributed by + Christian Göttsche + * Move pid file to /run (Closes: #925443) + * Add noahm@debian.org to Uploaders + * Work around flakiness in autopkgtest suite + * Suppress library-not-linked-against-libc lintian warnings some plugins as + false-positives + + -- Noah Meyerhans Wed, 10 Jun 2020 10:41:37 -0700 + +dovecot (1:2.3.7.2-1) unstable; urgency=medium + + * [dcaf24e] New upstream version 2.3.7.2 + - Fixes CVE-2019-11500 for dovecot-core + * [111beef] Update pigeonhole to 0.5.7.2 + - Fixes CVE-2019-11500 for pigeonhole/managesieve + * [a422c4c] Bump Standards-Version to 4.4.0; no changes needed + * [56e37ed] Bump dh compat to 12; no changes needed. + - Drop d/compat in favor debhelper-compat B-D. + * [476edbd] Refresh dovecot_name.patch and ssl-cert-location.patch + * [9dc7904] Drop patches included in 2.3.7.2. + - CVE-2019-10691 + - CVE-2019-11494 + - CVE-2019-11499 + - CVE-2019-7524 + - avoid-double-closing-mysql.patch + - lib-master-test-event-stats-Use-PRIu64-format.patch + + -- Apollon Oikonomopoulos Thu, 29 Aug 2019 11:55:51 +0300 + +dovecot (1:2.3.4.1-5) unstable; urgency=medium + + * [bd00402] Fix CVE-2019-11494 and CVE-2019-11499 (Closes: #928235) + - submission-login: fix null pointer dereference when client + disconnects during authentication (CVE-2019-11494) + - submission-login: fix assert-crash when receiving an invalid + authentication message over TLS (CVE-2019-11499) + + -- Apollon Oikonomopoulos Mon, 29 Apr 2019 23:35:05 +0300 + +dovecot (1:2.3.4.1-4) unstable; urgency=high + + * [d04d4ba] Fix assert-crash in JSON encoder (CVE-2019-10691) + + -- Apollon Oikonomopoulos Thu, 18 Apr 2019 10:21:19 +0300 + +dovecot (1:2.3.4.1-3) unstable; urgency=high + + * [07c9212] Fix two buffer overflows when reading oversized FTS headers + and/or oversized POP3-UIDL headers (CVE-2019-7524). + + -- Apollon Oikonomopoulos Mon, 25 Mar 2019 23:06:01 +0200 + +dovecot (1:2.3.4.1-2) unstable; urgency=medium + + [ Laurent Bigonville ] + * [ac99918] Fix double-free crash in mysql driver + Fix double closing of the connection in the mysql driver, this should + fix the crash in the dovecot auth process, taken from upstream. + (Closes: #918339) + + [ Apollon Oikonomopoulos ] + * [8a30446] Bump Standards-Version to 4.3.0; no changes needed + + -- Apollon Oikonomopoulos Thu, 14 Mar 2019 11:02:39 +0200 + +dovecot (1:2.3.4.1-1) unstable; urgency=high + + * [bebf0b4] New upstream version 2.3.4.1 + + Fixes CVE-2019-3814: TLS client auth username handling + + -- Apollon Oikonomopoulos Tue, 05 Feb 2019 16:19:12 +0200 + +dovecot (1:2.3.4-2) unstable; urgency=medium + + * [51d1317] Fix FTBFS on 32-bit platforms. + Cherry-pick upstream commit de42b54, fixing the event-stats test on + 32-bit platforms. + + -- Apollon Oikonomopoulos Sat, 24 Nov 2018 02:02:17 +0200 + +dovecot (1:2.3.4-1) unstable; urgency=medium + + * [14c247f] New upstream version 2.3.4 + * [7fed004] Update pigeonhole to 0.5.4 + + -- Apollon Oikonomopoulos Fri, 23 Nov 2018 22:00:06 +0200 + +dovecot (1:2.3.3-1) unstable; urgency=medium + + [ Jelmer Vernooij ] + * Trim trailing whitespace. + + [ Apollon Oikonomopoulos ] + * [6591a99] New upstream version 2.3.3 + * [3d718ec] Bump Standards-Version to 4.2.1; no changes needed + * [123bd32] Update pigeonhole to 0.5.3 + + -- Apollon Oikonomopoulos Thu, 04 Oct 2018 17:29:40 +0300 + +dovecot (1:2.3.2.1-1) unstable; urgency=medium + + * [40ba9f0] New upstream bugfix release 2.3.2.1 + * [87045ac] Drop fix-ftbfs-on-32bit.patch; merged upstream + * [5bb22a4] Bump Standards-Version to 4.1.5; no changes needed + + -- Apollon Oikonomopoulos Tue, 10 Jul 2018 17:51:43 +0300 + +dovecot (1:2.3.2-2) unstable; urgency=medium + + * [48067de] Fix FTBFS on 32-bit platforms by cherry-picking upstream commit + 1e23986f. + + -- Apollon Oikonomopoulos Wed, 04 Jul 2018 12:43:14 +0300 + +dovecot (1:2.3.2-1) unstable; urgency=medium + + * [bb03669] New upstream version 2.3.2 + + Upload to unstable + * [d29da3a] Merge 2.3 package changes from experimental. Important changes: + + [b3d1e17] Enable AppArmor support, see + https://wiki2.dovecot.org/Plugins/Apparmor + • B-D on libapparmor-dev + + [c0c55bd] Enable Lua scripting support for authdb/passdb. + • B-D on liblua5.3-dev + • New binary package, dovecot-auth-lua + + [4f6792e] Build with sodium support, enabling the ARGON2I and ARGON2ID + password schemes. + • B-D on libsodium-dev + + [54347e7] Build with ICU support enabling FTS unicode normalization + • B-D on libicu-dev + + New dovecot-submissiond binary package for the dovecot submission agent; + see https://wiki2.dovecot.org/Submission. + * [4db4813] Change maintainer address to dovecot@packages.d.o + * [5118354] Update pigeonhole to 0.5.2 + * [52a7af4] Drop murmur3-big-endian.patch; merged upstream + * [22a6eee] Refresh dovecot_name.patch + * [3af7568] dovecot_name.patch: apply to submissiond as well + + -- Apollon Oikonomopoulos Wed, 04 Jul 2018 08:57:45 +0300 + +dovecot (1:2.2.36-1) unstable; urgency=medium + + * [19f2274] d/gbp.conf: set merge-mode to "merge" to preserve pigeonhole/ + when importing new dovecot sources + * [6b9bf0d] New upstream version 2.2.36 + * [be12f22] Bump pigeonhole version to 0.4.24 + + Remove new file under doc/rfc + + Ship the new imap_filter_sieve module in dovecot-sieve + * [b77be59] Bump Standards-Version to 4.1.4; no changes needed + + -- Apollon Oikonomopoulos Wed, 06 Jun 2018 09:31:49 +0300 + +dovecot (1:2.2.35-2) unstable; urgency=medium + + * [7665652] Use git-subtree to generate pigeonhole patch from git; add + single-debian-patch to d/source/local-options + * [bfa0f10] d/rules: specify libdir manually; previous upload moved modules + under /usr/lib/, which was bound to break existing setups + * [982e826] d/copyright: adjust pigeonhole path and bump years + + -- Apollon Oikonomopoulos Thu, 22 Mar 2018 16:56:40 +0200 + +dovecot (1:2.2.35-1) unstable; urgency=medium + + * [8108cba] New upstream version 2.2.35 + * [6cbbaa1] Update pigeonhole to 0.4.23 (Closes: #892137) + * [9ace5f2] Switch Vcs-* URLs to salsa.d.o + * [ef40625] d/rules: call configure via dh_auto_configure. + Thanks to Helmut Grohne (Closes: #885854) + * [a459455] Drop B-D on libcurl4-gnutls-dev; removed upstream since 2.2 + * [235af9d] Update upstream signing key + + -- Apollon Oikonomopoulos Tue, 20 Mar 2018 11:15:42 +0200 + +dovecot (1:2.2.34-2) unstable; urgency=high + + * [868dc65] Update pigeonhole to 0.4.22 + * Set urgency to high due to the security fixes in 2.2.34-1 + + -- Apollon Oikonomopoulos Fri, 02 Mar 2018 18:36:23 +0200 + +dovecot (1:2.2.34-1) unstable; urgency=medium + + * [f53dc9a] New upstream version 2.2.34 (Closes: #921529) + Fixes the following security issues: + + CVE-2017-15130: TLS SNI config lookups may lead to excessive memory + usage (Closes: #891820) + + CVE-2017-14461: rfc822_parse_domain information leak vulnerability + (Closes: #891819) + + CVE-2017-15132: auth client leaks memory if SASL authentication is + aborted (Closes: #888432) + * [0dc98c6] Do not patch all-settings.c; regenerate it at build time + instead. Thanks to Aki Tuomi! + * [e678e3b] Bump dh compat to 11 + + B-D on debhelper (>= 11~) + + Use dh_installsystemd instead of dh_systemd_enable + * [271b290] Bump Standards-Version to 4.1.3; no changes needed + * [3cd6715] d/copyright: bump upstream and debian years + * [380d1ac] Drop the ENABLED flag from /etc/default/dovecot (but let the + initscript handle it if it exists) + * [97d6fae] d/watch: switch upstream URL to https:// + + -- Apollon Oikonomopoulos Thu, 01 Mar 2018 10:55:49 +0200 + +dovecot (1:2.2.33.2-1) unstable; urgency=medium + + * [8216f38] New upstream version 2.2.33.2 + + -- Apollon Oikonomopoulos Sat, 11 Nov 2017 20:59:43 +0200 + +dovecot (1:2.2.33.1-1) unstable; urgency=medium + + * [dbd1132] New upstream version 2.2.33.1 + + [b3d1f2d] Refresh split-protocols.patch + + [e0de123] Update pigeonhole to 0.4.21 + * [ef6a1eb] Set mail_privileged_group to 'mail' by default (Closes: #711856) + * [aeb6cf3] d/copyright: convert to Format 1.0 + * [5961f9d] Use dh-autoreconf for both, dovecot and pigeonhole. + * [85f1f0f] Bump Standards to 4.1.1; no changes needed + + -- Apollon Oikonomopoulos Fri, 13 Oct 2017 16:28:14 +0300 + +dovecot (1:2.2.32-2) unstable; urgency=medium + + * [fa71c69] dovecot-core.postinst: remove dovecot-common's postrm + (Closes: #696382) + * [e835c67] Ship decode2text.sh as an example (Closes: #767313) + * [63fb486] Deprecate dovecot-dbg in favor of auto dbgsyms + * [36b44b9] Handle unsupported SSLv2/SSLv3 in the ssl_protocols setting + gracefully (Closes: #866752) + + -- Apollon Oikonomopoulos Tue, 19 Sep 2017 16:59:38 +0300 + +dovecot (1:2.2.32-1) unstable; urgency=medium + + * [6652d9c] New upstream version 2.2.32. + * [c9cb096] Update pigeonhole to 0.4.20. + * [b499950] dovecot-core: remove SSL key/cert symlinks on purge + (Closes: #867157) + * [dbdcc66] dovecot-core.postinst: ignore adduser errors (Closes: #867849) + * [476c950] Bump Standards to 4.1.0; no changes needed. + * [2914efa] Drop B-D on autotools-dev, it is depended on by debhelper 10. + * [305d022] Remove Fabio, Joel and Marco from Uploaders. Thanks for your work! + + -- Apollon Oikonomopoulos Tue, 12 Sep 2017 16:15:52 +0300 + +dovecot (1:2.2.31-1) unstable; urgency=medium + + * [9b058f3] New upstream version 2.2.31 + + [2b577c1] Bump pigeonhole version to 0.4.19 + * Enable TLS by default: + + [7ca4b1c] Update SSL cert location patch; cert/key should reside under + /etc/dovecot/private by default. + + [05d3d0f] Use ssl-cert-snakeoil certificates to setup SSL by default + (Closes: #376146, #786570) + + [862901f] dovecot-core.postinst: manage 10-ssl.conf using ucf + (Closes: #850538) + + [418df05] README.Debian: document the new TLS setup + + [47bade9] dovecot-core.NEWS: document TLS support + * [8356bc0] Handle /etc/dovecot/private mode using dpkg-statoverride + * dovecot-core.postinst: cleanup + + [afbd33f] dovecot-core.postinst: always call adduser + + [ee22dc5] dovecot-core.postinst: remove obsolete conffile handling + + [7bb298b] dovecot-core.postinst: do not remove the imapd user/group + * [815a2d1] README.Debian: cleanup + * [91115a3] Use noawait dpkg triggers (lintian warning) + * [e845cec] Add basic usage DEP-8 test, doing end-to-end tests involving + LDA, IMAP and POP3. + * [71c73ef] systemd: convert service to Type=simple and start after + network-online.target (Closes: #865546, #825562) + * [1534fac] dovecot.service: enable ProtectSystem=full + * [d276c69] B-D only on default-libmysqlclient-dev + + -- Apollon Oikonomopoulos Tue, 27 Jun 2017 18:18:12 +0300 + +dovecot (1:2.2.30.2-1) unstable; urgency=medium + + * [401e83d] New upstream version 2.2.30.2 + * [1ea8321] Bump pigeonhole version to 0.4.18 + * [97bf8ec] Drop CVE-2017-2669 patch + * [9c1bbe2] Drop fix-sha3-on-big-endian.patch + * [d3c607b] Refresh dovecot_name.patch + * [5c64268] Bump Standards to 4.0.0; no changes needed + * [0b884fc] Bump compat to 10 + + B-D on debhelper (>= 10) + + Drop B-D on dh-systemd, now provided by debhelper + + Run dh --without=autoreconf, since we use autotools-dev + + -- Apollon Oikonomopoulos Thu, 22 Jun 2017 22:22:59 +0300 + +dovecot (1:2.2.27-3) unstable; urgency=high + + * [117285a] Remove /etc/dovecot/README (Closes: #849290) + * [04e8ce3] auth: Do not double-expand key in passdb dict when + authenticating (CVE-2017-2669) (Closes: #860049) + + -- Apollon Oikonomopoulos Tue, 11 Apr 2017 00:46:54 +0300 + +dovecot (1:2.2.27-2) unstable; urgency=medium + + * [30586e3] Fix SHA3 on big-endian architectures. + + -- Apollon Oikonomopoulos Thu, 15 Dec 2016 22:24:56 +0200 + +dovecot (1:2.2.27-1) unstable; urgency=medium + + [ Jaldhar H. Vyas ] + * [b1e4693] Imported Upstream version 2.2.27 + + Includes fix for CVE-2016-8652 (Closes: #846605) + + [ Apollon Oikonomopoulos ] + * [b25993a] Drop patches merged upstream: + + call_openssl_cleanup_at_deinit.patch + + disable_sslv23.patch + + -- Apollon Oikonomopoulos Wed, 14 Dec 2016 21:48:46 +0200 + +dovecot (1:2.2.26.0-4) unstable; urgency=medium + + * [3015f35] Drop references to SSLv2 in the default SSL protocols (Closes: #844271) + + -- Apollon Oikonomopoulos Mon, 14 Nov 2016 17:55:26 +0200 + +dovecot (1:2.2.26.0-3) unstable; urgency=medium + + * [b03027b] Call OPENSSL_cleanup() on dcrypt_openssl unload. Fixes FTBFS + with OpenSSL 1.1.0c. + + -- Apollon Oikonomopoulos Sun, 13 Nov 2016 10:56:30 +0200 + +dovecot (1:2.2.26.0-2) unstable; urgency=medium + + * [9db7d1b] Fix upgrades from 2.2.25 (Closes: #843028) + + -- Apollon Oikonomopoulos Tue, 08 Nov 2016 15:06:16 +0200 + +dovecot (1:2.2.26.0-1) unstable; urgency=medium + + [ Apollon Oikonomopoulos ] + * [18fc181] New upstream version 2.2.26.0 (Closes: #828286, #834837) + * [3ecfd3c] Update pigeonhole to 0.4.16 + * [61ff825] Move libdovecot-ldap and libdict_ldap to dovecot-ldap (Closes: + #830135). + * [b3a1650] Ubuntu: disable -Bsymbolic-functions ld flag. + Thanks to Christian Ehrhardt + (Closes: #842151) (LP: #1636781) + * [5828ab1] B-D on default-libmysqlclient-dev (but keep plain + libmysqlclient-dev as an alternative to ease backports). + * [0086110] Drop DRAC plugin. + Thanks to Christian Ehrhardt + (Closes: #842153) + + [ Jaldhar H. Vyas ] + * [60808eb] Move aclocal *.m4 files into -dev package. + * [52fd869] Move lib95_imap_sieve_plugin.so into dovecot-sieve (Closes: + #832046). + + -- Apollon Oikonomopoulos Thu, 07 Jul 2016 10:17:58 +0200 + +dovecot (1:2.2.25-1) unstable; urgency=medium + + * [cc29a81] Imported Upstream version 2.2.25 + * [d19bcca] Updated pigeonhole patch to 0.4.14 + * [16db179] Merged in some features of the Ubuntu dovecot package. + + dovecot-core: added lsb-base dependency. + + dovecot-core: Added apport hook. + + dovecot-imapd,dovecot-pop3d: Added ufw profiles. + Thanks to Christian Erhardt + (Closes: #828864) + + -- Jaldhar H. Vyas Fri, 01 Jul 2016 17:07:03 -0400 + +dovecot (1:2.2.24-1) unstable; urgency=medium + + * [26020b6] Imported Upstream version 2.2.24 (Closes: #818652) + + -- Apollon Oikonomopoulos Mon, 09 May 2016 10:42:08 +0300 + +dovecot (1:2.2.23-1) unstable; urgency=medium + + [ Jaldhar H. Vyas ] + * Drop missing-expunges.patch, merged upstream + + [ Apollon Oikonomopoulos ] + * [8a01915] Imported Upstream version 2.2.23 + + -- Apollon Oikonomopoulos Tue, 12 Apr 2016 17:30:03 +0300 + +dovecot (1:2.2.22-1) unstable; urgency=medium + + [ Jaldhar H. Vyas ] + * [2321581] Imported Upstream version 2.2.22 + * [3fa8a62] Updated pigeonhole patch to 0.4.13 + + -- Jaldhar H. Vyas Fri, 18 Mar 2016 19:18:34 -0400 + +dovecot (1:2.2.21-1) unstable; urgency=medium + + [ Jaldhar H. Vyas ] + * [d9c0630] Imported Upstream version 2.2.21 (Closes: #809666, #708539, + #801346, 803223) + * [5360548] Updated pigeonhole patch to 0.4.12 + * [5e6783f] Fixed typo in dovecot-core.README.Debian. + Thanks to Ingo Wichmann (Closes: #809717) + * [d00d0c7] Create /var/lib/dovecot in the package. (Closes: #801752) + * [6510373] Upstream patch for sync problem which could cause expunged + messages to keep reappearing. (Closes: #684499) + * [040f7fa] dovecot-core: dovecot.socket not enabled on installation + (Closes: #814999) + * [fc29003] /etc/dovecot/10-ssl.conf no longer managed by ucf or modified by + postinst. Thanks to Santiago Vila (Closes: #773237) + * [0d16e16] Fixed some lintian warnings. + * [801ba7e] Added Apollon to uploaders + + [ Apollon Oikonomopoulos ] + * [99ef3d8] Build with lz4 support (Closes: #784321) + * [92f68aa] Fix nss userdb (Closes: #712764) + * [41b9bde] Disable dovecot.socket in existing installations. + * [49c5b97] d/rules: specify systemd unit dir manually (Closes: #720854) + * [a377ccb] Convert to dh sequencer + * [48af954] B-D on debhelper >= 9 + * [591c315] Bump standards to 3.9.7; no changes needed + * [ceb629e] Use dh_installinit --name + * [ef8d8ac] d/rules: refactor file installation + * [a899bcc] dovecot-core: use dh_installman + * [6159e00] d/rules: build in parallel if requested + * [51014e6] d/control: use HTTPS Vcs-* URLs + * [b385cf5] dovecot-sieve: replace Conflicts with Breaks + * [b4a9e68] Add basic DEP-8 tests + * [7452649] Add DEP-8 tests for systemd support + * [e5101aa] Re-enable PIE and bindnow + * [5717808] Fix invoke-rc.d calls and never call init.d directly + * [3504241] Drop debconf remains + + -- Jaldhar H. Vyas Tue, 01 Mar 2016 19:31:42 -0500 + +dovecot (1:2.2.20-1) UNRELEASED; urgency=medium + + * [68d5038] Imported Upstream version 2.2.20 + + -- Jaldhar H. Vyas Sun, 13 Dec 2015 10:21:21 -0500 + +dovecot (1:2.2.19-1) UNRELEASED; urgency=medium + + * [146ef57] Imported Upstream version 2.2.19 + * [3af7cad] Updated pigeonhole patch to 0.4.9 + + -- Jaldhar H. Vyas Sun, 13 Dec 2015 09:41:56 -0500 + +dovecot (1:2.2.18-2) unstable; urgency=high + + * [3f3bf71] Updated pigeonhole patch to 0.4.8 (Closes: #792669) + + -- Jaldhar H. Vyas Sun, 23 Aug 2015 23:16:28 -0400 + +dovecot (1:2.2.18-1) unstable; urgency=medium + + * [cce20a5] Imported Upstream version 2.2.18. Closes: #786760 + * [36d2ec1] Refresh patch dovecot_name.patch. + * [109e6f8] Drop patch cve-2015-3420.patch: applied upstream. + * [6c59f09] Depend on krb5-multidev rather than libkrb5-dev. + + -- Jelmer Vernooij Sun, 24 May 2015 15:01:19 +0000 + +dovecot (1:2.2.16-1) unstable; urgency=medium + + * [e9d9193] Imported Upstream version 2.2.16 + * [976c256] Remove gbp- prefix from section names in debian/gbp.conf. + * [762b9a6] Add Dutch translation. Thanks, Frans Spiesschaert. Closes: #766203 + * [dea3dd6] Drop bye_logout_not_sent.patch: already included upstream. + + -- Jelmer Vernooij Mon, 04 May 2015 12:23:05 +0000 + +dovecot (1:2.2.13-12) unstable; urgency=high + + * [48f6fe4] Add patch cve-2015-3420.patch: Fix SSL/TLS handshake failures + leading to a crash of the login process with newer versions of OpenSSL. + Closes: #783649 (CVE-2015-3420) + + -- Jelmer Vernooij Mon, 04 May 2015 11:38:30 +0000 + +dovecot (1:2.2.13-11) unstable; urgency=high + + * [ebc0377] Don't allow install of dovecot-sieve without a new enough + dovecot-core. (Closes: #772885) + + -- Jaldhar H. Vyas Sun, 14 Dec 2014 12:27:50 -0500 + +dovecot (1:2.2.13-10) unstable; urgency=high + + [ Jelmer Vernooij ] + * [93db7f0] Fix path to 90-sieve-extprograms.conf in dovecot-sieve.postinst. + Closes: #772703, #772711 + + [ Jaldhar H. Vyas ] + * [8c627a4] Add another db_stop this time for triggers. Die #770695 die! + + -- Jaldhar H. Vyas Wed, 10 Dec 2014 19:56:20 -0500 + +dovecot (1:2.2.13-9) unstable; urgency=high + + * [5b689f6] Made some overlooked configuration files into conffiles; + deleted excess files from /usr/share/doc/dovecot-core. + * [83f2fc4] Explicitly stop debconf in dovecot-core postinst which hopefully + fixes the last of the install hangs. + Thanks to Chris Gilbert (Closes: #770695) + + -- Jaldhar H. Vyas Tue, 09 Dec 2014 06:17:23 -0500 + +dovecot (1:2.2.13-8) unstable; urgency=medium + + * [b2f652f] Turn off SSL by default and leave SSL cert locations commented + out. Hopefully this will be a satisfactory lowest common denominator for + new installs without messing with upgrades. + (Closes: #771334, #771407) + + -- Jaldhar H. Vyas Tue, 02 Dec 2014 00:21:30 -0500 + +dovecot (1:2.2.13-7) unstable; urgency=high + + * [daea09a] Commented out cert locations so install doesn't bomb if the + certs haven't been created yet. + (Closes: #706216, #732263, #767154, #768253, #769461, #770697) + + -- Jaldhar H. Vyas Fri, 28 Nov 2014 00:27:03 -0500 + +dovecot (1:2.2.13-6) unstable; urgency=medium + + * [f7205c0] dovecot-dev: removed dependency on dovecot-core + * [e393868] Removed SSL certificate generation from postinst. From now on + you have to do this yourself. See dovecot-core's README.debian for + instructions. (Closes: #730828) + + -- Jaldhar H. Vyas Sat, 25 Oct 2014 23:31:42 -0400 + +dovecot (1:2.2.13-5) unstable; urgency=medium + + * Fix name of organizationName field in SSL configuration for self- + signed certs. Closes: #760653 + + -- Jelmer Vernooij Sat, 06 Sep 2014 23:19:51 +0200 + +dovecot (1:2.2.13-4) unstable; urgency=medium + + * Add Provides with dovecot ABI version, for plugins to depend on. + Closes: #456021 + + -- Jelmer Vernooij Sun, 20 Jul 2014 19:31:09 +0200 + +dovecot (1:2.2.13-3) unstable; urgency=medium + + * Build-depend on clucene 2.3 or later, which upstream lists as the + minimum version. Closes: #754141 + * Use configuration file for openssl certificate configuration. + * Use canonical address for Vcs-Browser header. + * Remove unnecessary asterisk from NEWS entry; fixes lintian warning. + * Remove unused lintian override for usr/lib/dovecot/imap. + * dovecot-core: Ignore lintian warnings for empty directories. + + -- Jelmer Vernooij Tue, 08 Jul 2014 01:48:08 +0200 + +dovecot (1:2.2.13-2) unstable; urgency=medium + + * [bd5e34b] Patches from upstreams' hg repo to fix BYE and LOGOUT not being + sent. (Closes: #751682) + * [452e336] Czech translation for debconf. + Thanks to Michal Å imůnek (Closes: #751389) + + -- Jaldhar H. Vyas Wed, 25 Jun 2014 01:38:59 -0400 + +dovecot (1:2.2.13-1) unstable; urgency=medium + + * [0680040] Imported Upstream version 2.2.13 + + -- Jelmer Vernooij Sun, 25 May 2014 18:45:38 +0200 + +dovecot (1:2.2.13~rc1-1) unstable; urgency=medium + + * [7751508] Imported Upstream version 2.2.13~rc1 + + Fixes denial of service vulnerability (CVE-2014-3430). Closes: #747549 + + -- Jelmer Vernooij Sat, 10 May 2014 14:34:14 +0200 + +dovecot (1:2.2.12-3) unstable; urgency=medium + + * [0383db0] Break long line. + * [c961b6a] Fix installation of sieve plugins. Closes: #742770, #684271 + * [388d2bf] Strip body.rfc5173.txt from the pigeonhole patch, as it is + non-free. Closes: #745398 + + -- Jelmer Vernooij Wed, 23 Apr 2014 02:56:56 +0200 + +dovecot (1:2.2.12-2) unstable; urgency=medium + + * [c882a38] Add build dependencies libstemmer-dev and libexttextcat-dev, + used by dovecot-lucene. + * [67762d4] Use autotools-dev to update config.guess and config.sub. + * [33e79b7] Update Japenese po file. Thanks, victory. Closes: #730171 + * [cbf213b] Add non-standard-dir-perm override for /etc/dovecot/private. + + -- Jelmer Vernooij Sun, 23 Mar 2014 17:57:04 +0000 + +dovecot (1:2.2.12-1) experimental; urgency=medium + + * [52b67de] Update watch file for 2.2 series. + * [c4eac1f] Verify upstream tarball signature in watch file. + * [3a81ff9] Imported Upstream version 2.2.12 + * [8950a86] Bump standards version to 3.9.5 (no changes). + * [cd82417] Add myself to uploaders. + + -- Jelmer Vernooij Thu, 06 Mar 2014 22:45:19 +0000 + +dovecot (1:2.2.10-1) unstable; urgency=low + + * [0496c52] Imported Upstream version 2.2.10 + * [515dd61] Added new package for Lucene full text search support. + Thanks to Jelmer Vernooij for the patch. + (Closes: #685979) + * [718a68e] Fix old private key location in README.Debian. + Thanks to Jelmer Vernooij for the patch. + (Closes: #702385) + * [42da568] dovecot-solr: Make sure solr-schema.xml gets installed. + (In /usr/share/dovecot) + (Closes: #695185) + * [77acbf7] Added /usr/share/dovecot/dovecot-abi file in dovecot-dev to + document -- you guessed it! -- the dovecot ABI version. + * [feb597a] Added support for xz compression. + + -- Jaldhar H. Vyas Thu, 06 Mar 2014 02:51:34 -0500 + +dovecot (1:2.2.9-1) unstable; urgency=low + + [ Jaldhar H. Vyas ] + * [77468cf] Imported Upstream version 2.2.9 + * [43e08f3] Place dovenull user in its own group. (Closes: #725164) + * [e1a3e9c] Handled the fact that dovecot-db.conf.ext is no longer used. + (Closes: #728107, #730403) + + [Debconf translation updates] + * Russian (Yuri Kozlov). (Closes: #729106) + * German (Chris Leick). (Closes: #729358) + * Danish (Joe Hansen). (Closes: #729425) + * French (Julien Patriarca). (Closes: #729966) + * Portuguese (Américo Monteiro). (Closes: #730006) + * Polish (Michał Kułach). (Closes: #730061) + * Italian (Beatrice Torracca). (Closes: #730136) + * Japanese (victory). (Closes: #73017) + * Swedish (Martin Bagge / brother). (Closes: #730188) + * Spanish; (Camaleón). (Closes: #730354) + + -- Jaldhar H. Vyas Thu, 16 Jan 2014 15:42:13 -0500 + +dovecot (1:2.2.8-1) UNRELEASED; urgency=low + + * [6157a2b] New upstream version 2.2.8 + + -- Jaldhar H. Vyas Thu, 21 Nov 2013 16:54:46 -0500 + +dovecot (1:2.2.5-1) experimental; urgency=low + [ Micah Anderson ] + * [a0035bf] New upstream version 2.2.5 + * [a053c49] Update pigeonhole patch to 0.4.1 + * [689cd67] refreshed patches + + [ Jaldhar H. Vyas ] + * Caused bugs and then fixed them again. + + -- Jaldhar H. Vyas Mon, 09 Sep 2013 00:57:32 -0400 + +dovecot (1:2.1.17-2) unstable; urgency=low + + * [e8286e0] New version of drac patch taken from Ubuntu which works better + with 2.x (Closes: #716764) + * [23acb40] Add a patch from Ubuntu to report the distro name in the login + banner why not. + * [f8d566e] Don't need dovecot-common package anymore; get rid of it. + + -- Jaldhar H. Vyas Sat, 07 Sep 2013 14:58:05 -0400 + +dovecot (1:2.1.17-1) experimental; urgency=low + + [ Jaldhar H. Vyas ] + * [fa0d6aa] Re-enable mbox write locking patch to comply with policy 11.6 + (Closes: #720502) + * [38691fb] New upstream version (Closes: #719021) + * [1361144] prompts in dovecot-core postinst debconfiscated. + * IN MEMORIAM: Goldy the Goldfish (2000-2013) You were a prince (or + perhaps princess?) among fish and we shall all miss you dearly. + May your karmas merit much punya in future lives. + + -- Jaldhar H. Vyas Fri, 23 Aug 2013 01:46:20 -0400 + +dovecot (1:2.1.16-1) experimental; urgency=low + + * [9741bd8] New Upstream version + * [3476489] Updated pigeonhole patch to 0.3.5 + * [d4f236f] Removed some patches which are no longer required. + + -- Jaldhar H. Vyas Fri, 14 Jun 2013 16:25:19 -0400 + +dovecot (1:2.1.7-8) experimental; urgency=low + + * This version is not actually intended for upload. It merely undoes + some changes made for the the wheezy release. Namely, the following + features are back: + - TCP Wrappers support + - Hurd compatibility support + - Triggers. + + -- Jaldhar H. Vyas Thu, 13 Jun 2013 16:14:21 -0400 + +dovecot (1:2.1.7-7) unstable; urgency=high + + * If you are upgrading from stable or earlier versions of this package + from testing/unstable please carefully read + /usr/share/doc/dovecot-core/README.Debian.gz for important information + about changes. + * [0d74b31] Move Breaks/Replaces mailavenger from dovecot-common to + dovecot-core (Closes: #694376) + * [a8030a1] Revamped dovecot-cores README.Debian by adding any info I could + think of in order to ease upgrade problems. (Closes: #696820) + * [04798d3] Don't touch 10-ssl.conf at all. Eventually I will DTRT with + regards to the default generated ssl certificates but in the mean time + this will do the least mischief. (Closes: #696817) + * [fde17d1] Patch to make /etc/dovecot/readme point to the right place for + the example configuration. (Closes: #698941) + + -- Jaldhar H. Vyas Mon, 04 Feb 2013 16:27:17 -0500 + +dovecot (1:2.1.7-6) unstable; urgency=high + + * WARNING: in order to get this package into wheezy some functionality + from the previous release had to be removed. Namely: + - TCP Wrappers support + - Hurd compatibility support + - Triggers + All this will be coming back in the next version but for now, if you need + any of it, stick with -5. + * [1f869e0] NEWS.Debian was not getting added to the package (Closes: #693621) + * [564c5e2] Add breaks and replaces mailavenger for debian-common + (Closes: #694376) + + -- Jaldhar H. Vyas Fri, 14 Dec 2012 17:01:33 -0500 + +dovecot (1:2.1.7-5) unstable; urgency=high + + * [132bc3b] Remove call to ntp-wait in init script. Dovecot handles a + skewed clock much better now. (Closes: #693225) + + -- Jaldhar H. Vyas Wed, 14 Nov 2012 14:45:29 -0500 + +dovecot (1:2.1.7-4) unstable; urgency=high + + * [68ef7ad] piuparts complained /etc/dovecot/private was left unowned on + purge which is against policy (Closes: #692944) + + -- Jaldhar H. Vyas Sun, 11 Nov 2012 23:45:07 -0500 + +dovecot (1:2.1.7-3) unstable; urgency=high + + [ Jaldhar H. Vyas ] + * [03d5499] Do not generate new dovecot cert if key or cert is already + present (Closes: #685896,#631257) Thanks to Alexander Ufimtsev + and Jörg-Volker Peetz for patches. + * [5be6c2a] You should be able to upgrade and remove dovecot-managesieved + without errors (Closes: #665487) + * [1a9ad5c] Fixes instances where UTF-8 was misused instead of mUTF-7 + (Closes: #680035) + * [c0ac3ba] Backport of fix for failure to autocreate mailboxes + (Closes: #623440) This also requires setting the default mail_location. + * [ba1d3f5] Fix FTBFS on Hurd (Closes: #686931) via upstream patch backported + from mercurial. + * [cfa92b5] Upgrade pigeonhole page to 0.3.1 (Closes: #688407) + * [58f01c2] Generated certificates will now be created in /etc/dovecot + (Closes: #608719) + * [8e40ea5] Patch to build with PIE and bindnow + by intrigeri@debian.org (Closes: #679017) + * [87d982b] Use libwrap (Closes: #685850) + * [ebb5421] Start after nslcd (Closes: #692632) + * [2db5c1a] Add support for dpkg triggers. This means dovecot will not be + repeatedly restarted when installing or removing lots of plugins + (Closes: #601744) + * [bc66629] Patch to document -k in dsync.1 + by Luca Capello (Closes: #680992) + * [ba67766] Add lintian overrides. + + [ Marco Nenciarini ] + * [ffba408] Updated watch file + * [9777434] Install missing default configuration files from upstream + * [9496828] Backport fix for segfault in managesieve triggered by + CHECKSCRIPT command. (Closes: #688197) + + -- Jaldhar H. Vyas Sat, 10 Nov 2012 03:50:30 -0500 + +dovecot (1:2.1.7-2) unstable; urgency=low + + * [e23a136] Just a quick upload to make dovecot buildable on non-linux + platforms again. (Closes: #676817) I did it by removing the systemd + build-dep altogether. sd-daemon.[ch] is included in the source so if + systemd is installed on a system, it should be detected and socket + activation should happen. That is, assuming I've got everything right + (and documented) which is something I'll be looking into for the next + version. + + -- Jaldhar H. Vyas Thu, 21 Jun 2012 23:54:47 -0400 + +dovecot (1:2.1.7-1) unstable; urgency=low + + * [7668742] Imported upstream 2.1.7 (Closes: #663243) + + doveadm.1 documents the move command. (Closes: #641750) + * [4db927a] Patch to enable systemd support. (Closes: #672266) + Thanks Riku Voipio. + * [e17ac86] Previous attempt at setting hardening flags did not include the + drac plugin. Simon Ruderich provided an extra patch. + (Closes: #653530) + + -- Jaldhar H. Vyas Wed, 30 May 2012 16:27:21 -0400 + +dovecot (1:2.1.4-1) experimental; urgency=low + + * [6cc1e7d] Imported upstream 2.1.4 + + -- Micah Anderson Tue, 10 Apr 2012 11:33:05 -0400 + +dovecot (1:2.1.3-1) experimental; urgency=low + + * [49ecc33] Imported upstream 2.1.3 + + -- Micah Anderson Fri, 16 Mar 2012 22:04:46 -0400 + +dovecot (1:2.1.2-1) experimental; urgency=low + + * [70c3f6e] Imported upstream 2.1.2 + * Add missing dependency on dpkg-dev 1.16.1 + + -- Micah Anderson Thu, 15 Mar 2012 20:26:35 -0400 + +dovecot (1:2.1.1-1) experimental; urgency=low + + * [e113636] Imported upstream 2.1.1 + * Updated pigeonhole patch to 0.3.0, supporting 2.1 + + -- Micah Anderson Tue, 13 Mar 2012 23:08:06 -0400 + +dovecot (1:2.0.18-1) unstable; urgency=low + + * [85ae320] Imported Upstream version 2.0.18 + * [9cfd1da] Upped standards version to 3.9.3 + * [afb4164] Patch to dovecot-core/postinst so that permissions of symlinked + certificates aren't modified. (Closes: #646508) + Thanks Michael Kuhn. + * [bf642ee] Patch to enable hardened build flags. (Closes: #653530) + Thanks Moritz Muehlenhoff. + * [00b0d0c] Updated pigeonhole to 0.2.6 + + -- Jaldhar H. Vyas Fri, 09 Mar 2012 00:55:13 -0500 + +dovecot (1:2.0.15-1) unstable; urgency=low + + * [a22575a] New upstream version 2.0.15: (Closes: #642045) + + doveadm altmove: Added -r parameter to move mails back to primary + storage. + - v2.0.14: Index reading could have eaten a lot of memory in some + situations + - doveadm index no longer affects future caching decisions + - mbox: Fixed crash during mail delivery when mailbox didn't yet have + GUID assigned to it. + - zlib+mbox: Fetching last message from compressed mailboxes crashed. + - lib-sql: Fixed load balancing and error + * [8ce5abc] Update pigeonhole to release 0.2.4 + * [87658d2] Add dovecot-solr to dovecot-core's Suggests line. + + -- Marco Nenciarini Mon, 19 Sep 2011 19:26:48 +0200 + +dovecot (1:2.0.14-3) unstable; urgency=low + + * [f37a9ec] Enable solr full text search plugin. (LP: #620959) + * [cdd8b84] Build dovecot-common as architecture-all package. + * [af90444] Fix mail_plugin_dir default value in conf.d/10-mail.conf + (Closes: #624294) + * [61347cb] Bump debhelper build-depends to (>= 7.2.3~) because we use + dh_bugfiles. + * [4b757b6] Fix wrong configuration files path in sieve manpages. + + -- Marco Nenciarini Fri, 16 Sep 2011 02:26:21 +0200 + +dovecot (1:2.0.14-2) unstable; urgency=low + + [ Marco Nenciarini ] + * [a6896d6] Rename dovecot-common to dovecot-core. (Closes: #624835) + * [c36ca96] Manage protocols during package configuration and + deconfiguration phases. + * [599b55b] Update pigeonhole to release 0.2.3. + * [c654db0] Add a bug-script which will append doveconf -n output to bug + reports. + * [5270323] Add notes about upgrading from 1.2 to README.Debian. + (Closes: #635351) + * [5cd77a3] Add build-arch and build-indep target to debian/rules as + required by lintian. + * [be05d2a] Avoid ucf question when upgrading from squeeze and the only + difference in dovecot.conf is at the "protocols" line. + * [3c73782] Remove ucf backups during purge. + * [3ccf508] Make /etc/dovecot/*.ext readable by dovecot group members. + (Closes: #639005) + * [665bcc5] Use -c ${CONF} when calling doveconf to determine PIDBASE. + Thanks to Jonathan Hall (Closes: #627794) + * [325042c] Purge obsolete unmodified config files from /etc. + (Closes: #629397) + * [a9b9afb] Modified init script to report failures. (Closes: #629654) + * [d4622fd] Make dovecot-core suggesting all other packages containing + additional features. + + -- Marco Nenciarini Thu, 15 Sep 2011 19:43:55 +0200 + +dovecot (1:2.0.14-1) unstable; urgency=low + + * [50a947e] New upstream version 2.0.14: (Closes: #640143) + + doveadm: Added support for running mail commands by proxying to + another doveadm server. + + Added "doveadm proxy list" and "doveadm proxy kick" commands to + list/kick proxy connections (via a new "ipc" service). + + Added "doveadm director move" to assign user from one server to + another, killing any existing connections. + + Added "doveadm director ring status" command. + + userdb extra fields can now return name+=value to append to an + existing name, e.g. "mail_plugins+= quota". + - script-login attempted an unnecessary config lookup, which usually + failed with "Permission denied". + - lmtp: Fixed parsing quoted strings with spaces as local-part for + MAIL FROM and RCPT TO. + - imap: FETCH BODY[HEADER.FIELDS (..)] may have crashed or not + returned all data sometimes. + - ldap: Fixed random assert-crashing with with sasl_bind=yes. + - Fixes to handling mail chroots + - Fixed renaming mailboxes under different parent with FS layout when + using separate ALT, INDEX or CONTROL paths. + - zlib: Fixed reading concatenated .gz files. + * [0297425] Use xz compression for dbg packages. + * [4464ccf] Support parallel builds in debian/rules + * [19667f0] Acknowledge NMU 1:2.0.13-1.1. + Thanks to Luk Claes + + -- Marco Nenciarini Tue, 06 Sep 2011 23:18:46 +0200 + +dovecot (1:2.0.13-1.1) unstable; urgency=low + + * Non-maintainer upload. + * Don't ship .la files (Closes: #621297). + + -- Luk Claes Sat, 18 Jun 2011 12:31:28 +0200 + +dovecot (1:2.0.13-1) unstable; urgency=high + + [ Marco Nenciarini ] + * [8af9e4d] New upstream version 2.0.13: + + Added "doveadm index" command to add unindexed messages into + index/cache. If full text search is enabled, it also adds unindexed + messages to the fts database. + + added "doveadm director dump" command. + + pop3: Added support for showing messages in "POP3 order", which can + be different from IMAP message order. This can be useful for + migrations from other servers. Implemented it for Maildir as 'O' + field in dovecot-uidlist. + - doveconf: Fixed a wrong "subsection has ssl=yes" warning. + - mdbox purge: Fixed wrong warning about corrupted extrefs. + - sdbox: INBOX GUID changed when INBOX was autocreated, leading to + trouble with dsync. + - script-login binary wasn't actually dropping privileges to the + user/group/chroot specified by its service settings. + - Fixed potential crashes and other problems when parsing header names + that contained NUL characters. (CVE-2011-1929) + (Closes: #627443) + + -- Marco Nenciarini Sat, 21 May 2011 23:58:06 +0200 + +dovecot (1:2.0.12-1) unstable; urgency=low + + [ Jaldhar H. Vyas ] + * [66cbb94] New upstream major release. Imported Upstream version 2.0.12 + * [a48c7fd] Updated standards version + + [ Marco Nenciarini ] + * [5c6b334] Updated watch file + * [0ba683e] Updated init script to new configuration scheme. (Closes: #593527) + * [8370be4] Fix upgrading from rename-it.nl packages + * [ae804e3] Update changelog + + -- Jaldhar H. Vyas Mon, 25 Apr 2011 17:03:26 -0400 + +dovecot (1:2.0.11-1) experimental; urgency=low + + * [a4764a4] New upstream major release (Closes: #573889, #580929, + #597476, #603951, #606089, #606646, #606649, #608075, #612369, #610919) + * [461f667] Updated copyright and package descriptions. Thanks Stephan + Bosch. + * [7fbd92a] Drop all patches but drac support. Update pigeonhole to + release 0.2.1 + * [05a5a1e] Update debian/rules to work with dovecot 2.0. Thanks + Stephan Bosch. + * [a36193e] Update debian/dovecot-common.{postinst,postrm} to handle + the new config file layout. Thanks Stephan Bosch. + * [3589afd] Remove debian/dovecot.1 manpage because is provided by + upstream. + * [f1703e1] Remove debian/dovecotpw.1 manpage as there is no dovecotpw + executable in dovecot 2.0. Same feature is provided through 'doveadm + pw' command. + * [e30283d] Remove expire-tool.sh wrapper as there is no expire-tool + in dovecot 2.0. Same feature is provided through 'doveadm expunge' + command. + * [3385061] Remove daily cron script and the related configuration + from the default file. + * [bec19a0] Update debian/dovecot-common.NEWS.Debian with upgrading + instructions + * [1e56f1c] Call dh_makeshlibs with --no-script option to avoid + {postinst,postrm}-has-useless-call-to-ldconfig lintian errors. + * [325f055] Disable numbers in patches (requires git-buildpackage >=0.5.13) + * [74d6ebe] Added debian/source/local-options with "unapply-patches". + * [0edef17] Set default protocols value as empty and enable each + protocol in its own configuration file in + /usr/share/dovecot/protocols.d (LP: #661453) + * [e88860c] Removed code about upgrading from pre-etch packages. + * [95c5d47] Removed obsolete Build-Conflict (Closes: #614053) + * [41dddf4] Move configurations for imapd and pop3d to the appropriate + packages. (Closes: #606648) + * [b8b66dc] Removed obsolete README.Debian and NEWS.Debian from + dovecot-pop3d and dovecot-imapd packages + * [0bda8ac] Added packages dovecot-lmtpd and dovecot-managesieved. + * [9a954f1] Build databases, ldap and gssapi support as plugins + * [1a3f323] Deregister obsolete configuration files from ucf in + dovecot-common postinst on upgrades. + * [fae4b76] Added packages dovecot-pgsql, dovecot-mysql, dovecot-sqlite, + dovecot-ldap, dovecot-gssapi and dovecot-sieve (Closes: #251433) + * [aa8983d] Imported Upstream version 2.0.11 + + -- Marco Nenciarini Mon, 07 Mar 2011 18:23:59 +0100 + +dovecot (1:1.2.15-3) unstable; urgency=low + + * [096c373] Fix file overwrite error when upgrading from lenny + (Closes: #601980) + * [37b3220] Add --no-create-home to adduser invocation in dovecot- + common.postinst script (Closes: #601767) + * [b00b527] Fall back to `hostname` if `hostname -f` invocation fails + (Closes: #562780) + * [9102fec] Make self-generated certificates key length 2048 bits. + + -- Marco Nenciarini Tue, 02 Nov 2010 11:12:53 +0100 + +dovecot (1:1.2.15-2) unstable; urgency=low + + * [a0744a5] Switch vcs fields to git + * [d8a1bd4] Add debian/gbp.conf to make easy the usage of git- + buildpackage + * [c0dc21f] Warn that the generated SSL certificate will expire in 10 + years instead of 365 days, as it's the lifetime of of self-generated + certificates since 1:1.2.13-1 + * [6472d63] Switch to gbp-pq patch management. + * [bf2fc17] Upgraded sieve plugin to version 0.1.18 + * [6b9809d] Upgraded managesieve plugin to version 0.11.12 + * [d67cd17] Upgraded dovecot-managesieve.patch to version 0.11.12 + + -- Marco Nenciarini Thu, 28 Oct 2010 22:50:19 +0200 + +dovecot (1:1.2.15-1) unstable; urgency=high + + [ Marco Nenciarini ] + * New upstream release (Closes: #587036,#597529) + * Updated policy version to 3.9.1.0 (no changes needed) + + [ Jaldhar H. Vyas ] + * [SECURITY] Fixes two bugs with acls which could have allowed a user to + gain improper access or admin rights to shared mailboxes. + (Closes: #599521) + * Warn that the generated SSL certificate will expire in 365 days. Thanks + Phillip Weis. (Closes: #576455) + * Wherever the path to sendmail is given as /usr/lib/sendmail, change to + /usr/sbin/sendmail. (Closes: #570814,#595671) + + -- Jaldhar H. Vyas Fri, 08 Oct 2010 17:34:19 -0400 + +dovecot (1:1.2.13-1) unstable; urgency=low + + [ Jaldhar H. Vyas ] + * dovecot-common: Upped expiration date of self-generated certificates to + 10 years from 1 year. (Closes: #587371) + + [ Marco Nenciarini ] + * New upstream release: + - Fixed iconv() crash when it was processing several kilobytes of + broken continuous input. This mainly could have caused a problem + with IMAP SEARCH. Possibly also with some Sieve checks. + - If MIME encoded-words contained line feeds, Dovecot logged + cache corruption errors. + - mbox: Renaming mailbox under newly created dir didn't move index + directory. + - mbox: When generating envelope to From_-line, don't append a second + @owndomain if username already has one. + * Updated policy version to 3.9.0.0 (no changes needed) + + -- Marco Nenciarini Mon, 26 Jul 2010 17:03:38 +0200 + +dovecot (1:1.2.12-1) unstable; urgency=low + + * New upstream release + * Upgraded sieve plugin to version 0.1.17 + * Refreshed dovecot-managesieve.patch + * debian/patches/sieve-fix-imapflags.patch: Removed. Fixed upstream. + + -- Marco Nenciarini Sat, 26 Jun 2010 18:14:15 +0200 + +dovecot (1:1.2.11-1) unstable; urgency=high + + * New upstream release + * urgency set to high, because under particular circumstances you could + cause a DoS by sending a message with a huge header. + * sieve: Fix addflags command in deprecated imapflags extension + (Closes: #570058) + * Include a daily expire script with a setting in /etc/default/dovecot + to enable it. The new script is disabled by default as it requires + some configurations by the system administrator. Thanks to Hans Spaans + * Mark expire-tool.sh as a bash script + * Bump Standards-Version to 3.8.4, no changes needed + + -- Marco Nenciarini Mon, 08 Mar 2010 22:25:46 +0100 + +dovecot (1:1.2.10-1) unstable; urgency=low + + * New upstream release (Closes: #566796) + * Upgraded sieve plugin to version 0.1.15 + * Upgraded managesieve plugin to version 0.11.11 + * Upgraded dovecot-managesieve.patch to version 0.11.11 + + -- Marco Nenciarini Mon, 25 Jan 2010 17:21:09 +0100 + +dovecot (1:1.2.9-2) unstable; urgency=low + + * Upgraded sieve plugin to version 0.1.14 + * Upgraded managesieve plugin to version 0.11.10 + * Upgraded dovecot-managesieve.patch to version 0.11.10 + * debian/patches/dovecot-example.patch: Refreshed. + * Added expire-tool wrapper script (Closes: #565538) + * Added ${misc:Depends} to the dependencies of each binary package, + to make lintian happy (debhelper-but-no-misc-depends) + + -- Marco Nenciarini Sun, 17 Jan 2010 09:39:42 +0100 + +dovecot (1:1.2.9-1) unstable; urgency=low + + * New upstream release. + * debian/patches/gold-fix.patch: Removed. Fixed upstream. + + -- Marco Nenciarini Thu, 17 Dec 2009 10:52:53 +0100 + +dovecot (1:1.2.8-2) unstable; urgency=low + + * Moved libexec to lib corrections in dovecot-managesieve.patch and + dovecot-managesieve-dist.patch to dovecot-example.patch + * debian/patches/dovecot-mboxlocking.patch: Regenerated to avoid FTBFS + when quilt isn't installed. + * debian/patches/quota-mountpoint.patch: Removed. Not needed anymore. + * debian/patches/dovecot-quota.patch: Removed. Quotas aren't properly + enabled unless mail_plugins = quota imap_quota. + * debian/patches/gold-fix.patch: Fixed configure script to build even + with binutils-gold or --no-add-needed linker flag (Closes: #554306) + * debian/dovecot-common.init: fixed LSB headers. Thanks to Pascal Volk. + (Closes: #558040) + * debian/changelog: added CVE references to previous changelog entry. + * debian/rules: checked up the build system. It's not fragile anymore. + (Closes: 493803) + * debian/dovecot-common.postinst: Now invoking dpkg-reconfigure + on dovecot-common is enough to generate new certificates + if the previous ones were removed. (Closes: #545582) + * debian/rules: No longer install convert-tool in /usr/bin. + It isn't an user utility and it should stay in /usr/lib/dovecot + like all other similar tool. + + -- Marco Nenciarini Tue, 08 Dec 2009 12:24:04 +0100 + +dovecot (1:1.2.8-1) unstable; urgency=high + + [ Marco Nenciarini ] + * New upstream release. (Closes: #557601) + * [SECURITY] Fixes local information disclosure and denial of service. + (see: http://www.dovecot.org/list/dovecot-news/2009-November/000143.html + and CVE-2009-3897) + * Added myself to uploaders. + * Switched to the new source format "3.0 (quilt)": + - removed dpatch from build-depends + - removed debian/README.source because now we use only standard + dpkg features + - regenerated all patches + * Prepared to switch to multi-origin source: + - recreated dovecot-libsieve.patch and dovecot-managesieve-dist.patch + starting from the upstream tarball + - removed all autotools related build-depends and build-conflict + - renamed dovecot-libsieve and dovecot-managesieve directories + to libsieve and managesieve. + * debian/rules: Moved the configuration of libsieve and managesieve from + the build phase to the configuration phase + + [ Jaldhar H. Vyas ] + * Added dovecot-dbg package with debugging symbols. Thanks Stephan Bosch. + (Closes: #554710) + * Fixed some stray libexec'isms in the default configuration. + + -- Jaldhar H. Vyas Mon, 23 Nov 2009 17:04:14 -0500 + +dovecot (1:1.2.7-1) unstable; urgency=low + + * New upstream release. + * debian/dovecot-common.init: + - use $CONF when starting the daemon. (Closes: #549944) + - always output start/stop messages. (Closes: #523810) + + -- Fabio Tranchitella Sat, 14 Nov 2009 11:28:33 +0100 + +dovecot (1:1.2.6-1) unstable; urgency=low + + * New upstream release. + * debian/patches/v1.2.5-exec-mail_fix.dpatch: removed, merged upstream. + + -- Fabio Tranchitella Mon, 12 Oct 2009 06:56:55 +0000 + +dovecot (1:1.2.5-2) unstable; urgency=low + + * debian/dpatches/v1.2.5-exec-mail_fix.dpatch: added. + (Closes: #546694, #546695) + * debian/dovecot-common.dirs: removed /etc, /etc/ssl/{private,certs}, they + are handled by openssl; this should fix the piuparts test. + + -- Fabio Tranchitella Sun, 27 Sep 2009 20:53:52 +0200 + +dovecot (1:1.2.5-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Mon, 14 Sep 2009 14:34:06 +0200 + +dovecot (1:1.2.4-2) unstable; urgency=low + + * debian/patches/dovecot-libsieve.dpatch: updated to 0.1.12. + (Closes: #539527) + + -- Fabio Tranchitella Wed, 02 Sep 2009 07:28:23 +0200 + +dovecot (1:1.2.4-1) unstable; urgency=low + + * New upstream release. (Closes: #537186, #540058) + * Bumped Standards-Version to 3.8.3, no changes needed. + * debian/control: only suggests ntp. (Closes: #542152) + * debian/patches/dovecot-managesieve-dist.dpatch: updated to the 0.11.9 + release. (Closes: #539527) + * debian/rules: dovecot should be started with sequence number 20. + (Closes: #543473) + + -- Fabio Tranchitella Sat, 29 Aug 2009 17:27:59 +0200 + +dovecot (1:1.2.2-2) unstable; urgency=high + + * Er that should be fcntl not fnctl. (Closes: #539474, #539486) + * For the sake of completeness, fixed some errors in the example config file. + (They are in the !include statements which are commented out by default + so it is unlikely that most users will actually be affected.) + (Closes: #539391) + + -- Jaldhar H. Vyas Sat, 01 Aug 2009 11:38:03 -0400 + +dovecot (1:1.2.2-1) unstable; urgency=low + + [Joel Johnson] + * New upstream version, updated ManageSieve patch + - fixes index corruption condition (Closes: #537388) + * Set default for mbox_write_locks to "fnctl dotlock" according to + Debian Policy (Closes: #537326) + * Set MySQL build dependency to use version-agnostic -dev package + * Drop postgres-configure patch, functionality included upstream + * Updated dovecot-libsieve to version 0.1.9 + - fixes subaddress matching (Closes: #537386) + - check additional address-list headers (Closes: #537379) + + [Jaldhar H. Vyas] + * Remove unneeded build-dependencies on byacc and flex now that we no longer + use cmusieve. + + -- Joel Johnson Tue, 28 Jul 2009 20:35:57 -0600 + +dovecot (1:1.2.1-2) unstable; urgency=low + + * Update to use default automake (Closes: #536880) now that current upstream + successfully builds with 1.10 (was previously a problem in bug 473754. + * Updated Standards-Version to 3.8.2 (no action required) + * Remove ./var/run directories to fix lintian warnings. + * Updated dovecot-libsieve patch to version 0.1.8, removed autogen.sh, + tests, and doc/rfc + * Updated dovecot-managesieve patch to version 0.11.7 + * Updated dovecot-managesieve-dist patch to Mercurial revision 12b9733ee8b0 + (0.11.7+), removed lib-cmusieve, doc/rfs, autogen.sh + * Add additional upgrading note to README.Debian to clarify the ManageSieve + configuration changes needed - existing configurations break if not + updated! (Closes: #537158) + * Include the ChangeLog and README for sieve and ManageSieve + + -- Joel Johnson Wed, 15 Jul 2009 20:29:33 -0600 + +dovecot (1:1.2.1-1) unstable; urgency=low + + [ Joel Johnson ] + * New upstream release + * Update packaging for 1.2 release + - update SIEVE patch to use Dovecot rewrite (version 0.1.7, + removed RFCs and tests for a DFSG patch) + - update ManageSieve patch and software to hg revision 2c9b4b4ab6a8 + (0.11.6+) for 1.2.1 support + + removed lib-cmusieve since building against newer sieve + + removed doc/rfc for DFSG compliance + + removed autogen.sh since we run autotools during the build + - updated other misc patches + + -- Joel Johnson Sun, 12 Jul 2009 11:15:01 -0600 + +dovecot (1:1.1.16-1) unstable; urgency=low + + * New upstream release. (Closes: #531599) + + -- Fabio Tranchitella Thu, 04 Jun 2009 12:38:59 +0200 + +dovecot (1:1.1.15-1) unstable; urgency=low + + * New upstream release; sorry, no time to include extra upstream patches + which are not released, feel free to submit them as dpatch files and I will + add them to the package. (Closes: #529923) + * debian/control: updated Standards-Version, no changes needed. + * debian/dovecot-common.init: patched to support ENABLED=0 in + /etc/default/dovecot, useful if dovecot is used as local-only IMAP server + through PREAUTH interface. (Closes: #524302) + * debian/dovecot-common.README.Debian: simplified, we only point the user to + wiki.dovecot.org; added a note about how to regenerate the SSL certificate. + (Closes: #528934) + * debian/dovecot-common.postrm: do not break if /etc/ssl/certs does not + exist. (Closes: #524865) + * debian/patches/dovecot-managesieve-dist: updated to 0.10.6. + + -- Fabio Tranchitella Sun, 31 May 2009 20:13:26 +0200 + +dovecot (1:1.1.13-2) unstable; urgency=high + + * New upload, urgency set to high: the package in testing is broken. + * debian/dovecot-common.init: fixed a typo. + * debian/patches/dovecot-example.dpatch: fixed a few paths. + (Closes: #521544) + + -- Fabio Tranchitella Sun, 29 Mar 2009 21:16:02 +0200 + +dovecot (1:1.1.13-1) unstable; urgency=low + + * New upstream release. + * This version fixes problems accessing mailboxes in some setups. + (Closes: #520310) + + -- Fabio Tranchitella Thu, 19 Mar 2009 17:31:56 +0100 + +dovecot (1:1.1.12-1) unstable; urgency=low + + * New upstream release, it fixes some wrong defaults in the configuration + file. (Closes: #518631) + * debian/rules: add support for libdb. (Closes: #518630) + * debian/dovecot-common.postinst: create the dovecot certificate only at + install time. (Closes: #518738, #518598) + + -- Fabio Tranchitella Mon, 16 Mar 2009 14:26:32 +0100 + +dovecot (1:1.1.11-4) unstable; urgency=low + + * debian/dovecot-common.init: fixed a typo from the last upload. + (Closes: #518504) + + -- Fabio Tranchitella Fri, 06 Mar 2009 18:39:09 +0100 + +dovecot (1:1.1.11-3) unstable; urgency=low + + * debian/dovecot-common.init: applied patch from HÃ¥kon Stordahl to call + ntp-wait if available. (Closes: #517808) + + -- Fabio Tranchitella Wed, 04 Mar 2009 17:38:59 +0100 + +dovecot (1:1.1.11-2) unstable; urgency=low + + * debian/dovecot-common.init: fixed a bug in the init script, adding a + missing slash in the PIDFILE path. (Closes: #516845) + * debian/dovecot-common.init: fixed a bug in the init script, sending the HUP + signal for reloading the configuration. (Closes: #512197) + * debian/dovecot-common.init: check if /etc/inetd.conf exists. + (Closes: #509259, #497148) + * debian/control: dovecot-common suggests ntp and ntpdate. (Closes: #511060) + * debian/dovecot-common.postinst: migration hook for default_mail_env => + mail_location. (Closes: #517073) + * debian/dovecot.1: fixed the path of the configuration file. + (Closes: #501493) + + -- Fabio Tranchitella Fri, 27 Feb 2009 17:24:09 +0100 + +dovecot (1:1.1.11-1) unstable; urgency=low + + * New upstream release, upload to unstable. (Closes: #507122) + * debian/patches/dovecot-quota.dpatch: fixed a bug in the imap quota plugin. + (Closes: #484677) + * debian/dovecot-common.init: added the status action, thanks Fladischer + Michael for the patch. (Closes: #509694) + * debian/dovecotpw.1: added manpage for dovecotpw, thanks Xavier Luthi for + the patch. (Closes: #504712) + + -- Fabio Tranchitella Fri, 20 Feb 2009 20:39:38 +0100 + +dovecot (1:1.1.9-1) experimental; urgency=low + + [ Fabio Tranchitella ] + * debian/control: dovecot-common suggests ntp. + + [ Joel Johnson ] + * New upstream release + * updated managesieve patch to apply against new version + + -- Joel Johnson Sat, 24 Jan 2009 04:12:42 +0000 + +dovecot (1:1.1.8-1) experimental; urgency=low + + * New upstream release. + * debian/control: added LDA to the description of dovecot-common. + + -- Fabio Tranchitella Wed, 07 Jan 2009 23:14:29 +0100 + +dovecot (1:1.1.7-1) experimental; urgency=low + + * New upstream release + * Updated dovecot-ssh.patch for new release + * Updated MANAGESIEVE to 0.10.4 + * Fix package to support double compilation + - Properly clean dovecot-managesieve as pointed out by Stephan Bosch + - Add --copy directive to automake invocation + + -- Joel Johnson Thu, 04 Dec 2008 20:52:11 -0700 + +dovecot (1:1.1.2-3) experimental; urgency=low + + * debian/control: added libbz2-dev to the Build-Depends to enable the bzip2 + support. (Closes: #495129) + + -- Fabio Tranchitella Thu, 14 Aug 2008 20:45:41 +0200 + +dovecot (1:1.1.2-2) experimental; urgency=low + + * Merged changes in the unstable package. + * debian/control: added replaces for the imap and pop modules from the + -common to the the -imap and -pop packages. (Closes: #493798) + * debian/rules: applied some of the suggestions from Matthias Kloses, + thanks! (Refs: #493803) + + -- Fabio Tranchitella Tue, 05 Aug 2008 21:20:33 +0200 + +dovecot (1:1.1.2-1) experimental; urgency=low + + * New upstream release + * Trivial update to autocreate patch + + -- Joel Johnson Sat, 26 Jul 2008 08:18:39 -0600 + +dovecot (1:1.1.1-1) experimental; urgency=low + + [ Joel Johnson ] + * New upstream release (Closes: #487989) + * Updated policy version to 3.8.0.1 (no changes needed) + * Updated watch file to watch new release directory + * Actually install dovecot.8 since we have it + * Removed quota_v2 patch (was unreachable code): upstream now defaults + to quota v1 if _LINUX_QUOTA_VERSION is not defined + * Removed pam-error-information patch: included upstream + * Removed mbox_snarf patch: included upstream + * Removed full MANAGESIEVE patch (replaced with new 1.1 approach) + * Merged protocols_none_by_default patch into dovecot-example patch + * Updated drac patch to build against 1.1 + * Updated autocreate patch with 1.1 version + * Updated dovecot-sieve to 1.1.5 + * Added dovecot-managesieve patch against dovecot tree + * Added dovecot-managesieve source for building module + + [ Fabio Tranchitella ] + * Added Joel Johnson to the uploaders; thanks for your work! + + -- Fabio Tranchitella Sun, 20 Jul 2008 19:09:23 +0200 + +dovecot (1:1.0.15-2) unstable; urgency=low + + * debian/dovecot.1: added "This includes doing a syntax check" to the -a + option. + * debian/rules: now the package builds two times in a row without problems. + (Closes: #490201) + + -- Fabio Tranchitella Sun, 27 Jul 2008 19:56:18 +0200 + +dovecot (1:1.0.15-1) unstable; urgency=low + + * New upstream release. + * debian/rules: clean the package before unpatching. (Closes: #490201) + + -- Fabio Tranchitella Sun, 22 Jun 2008 09:06:01 +0200 + +dovecot (1:1.0.14-1) unstable; urgency=low + + * New upstream release. + * debian/patches/inbox_creation.dpatch: removed, merged upstream. + * debian/patches/allow_nets.dpatch: removed, merged upstream. + + -- Fabio Tranchitella Tue, 03 Jun 2008 10:07:13 +0200 + +dovecot (1:1.0.13-6) unstable; urgency=low + + * debian/patches/inbox_creation.dpatch: added, use mail_privileged_group's + group when creating inboxes if the unprivileged user fails; upstream: + http://hg.dovecot.org/dovecot-1.0/rev/932768a879c6 (Closes: #471716) + + -- Fabio Tranchitella Wed, 28 May 2008 12:58:17 +0200 + +dovecot (1:1.0.13-5) unstable; urgency=low + + * debian/patches/allow_nets.dpatch: added, allow_nets didn't work correctly + with big endian machines; patch from upstream, thanks Timo: + http://hg.dovecot.org/dovecot-1.0/rev/71c02fdf1b59 + + -- Fabio Tranchitella Thu, 15 May 2008 14:48:14 +0200 + +dovecot (1:1.0.13-4) unstable; urgency=low + + * debian/patches/dovecot-MANAGESIEVE-9.3.dpatch: updated managesieve to + version 9.3. + * debian/dovecot-common.README.Debian: added a note about how to configure + dovecot to log to file instead of using syslog. + * debian/dovecot.1: added a SIGNALS section. (Closes: #479059) + * dovecot-sieve: updated to the last hg release (5c3ba11994cb). + (Closes: #479104) + + -- Fabio Tranchitella Mon, 05 May 2008 17:28:21 +0200 + +dovecot (1:1.0.13-3) unstable; urgency=low + + * debian/rules: do not install anymore the ldap and sql example + configuration files under /etc. (Closes: #472674) + * debian/dovecot-common.postinst: really chmod + /etc/dovecot/dovecot-{ldap,sql}.conf files to 0600. + * debian/devecot-common.init: do not start the service if dovecot.conf + doesn't exist. (Closes: #475888) + + -- Fabio Tranchitella Sun, 27 Apr 2008 22:42:37 +0200 + +dovecot (1:1.0.13-2) unstable; urgency=low + + * debian/rules: use aclocal-1.9 instead of aclocal. (Closes: #473754) + + -- Fabio Tranchitella Tue, 01 Apr 2008 15:30:32 +0200 + +dovecot (1:1.0.13-1) unstable; urgency=high + + * New upstream release, fixes a security issue: + http://www.dovecot.org/list/dovecot-news/2008-March/000064.html + + -- Fabio Tranchitella Sun, 09 Mar 2008 19:22:20 +0100 + +dovecot (1:1.0.12-1) unstable; urgency=high + + * New upstream release. (Closes: #469457) + * debian/patches/dovecot-MANAGESIEVE-9.2.dpatch: updated, thanks to Marco + Nenciarini for the patch. + + -- Fabio Tranchitella Thu, 06 Mar 2008 15:53:07 +0100 + +dovecot (1:1.0.10-4) unstable; urgency=low + + * debian/patches/autocreate.dpatch: added, thanks to Walter Reiner. + * debian/rules: use --with-ioloop=best instead of --with-ioloop=epoll, as + suggested by Timo. (Closes: #466296) + + -- Fabio Tranchitella Mon, 18 Feb 2008 09:29:39 +0100 + +dovecot (1:1.0.10-3) unstable; urgency=low + + * debian/patches/dovecot-MANAGESIEVE-9.1.dpatch: added, thanks to Aleksey + Midenkov for providing a patch. (Closes: #416166) + * debian/dovecot-common.init: added $time to Should-Start. (Closes: #461543) + * debian/dovecot-common.postinst: do not add the dovecot user to the mail + group, it is not required by upstream. (Closes: #457123) + * debian/control: updated Standards-Version to 3.7.3, no changes required. + + -- Fabio Tranchitella Sun, 10 Feb 2008 18:37:55 +0100 + +dovecot (1:1.0.10-2) unstable; urgency=low + + * debian/patches/mbox_snarf.dpatch: added, thanks to Bernd Kuhls. + (Closes: #462319) + + -- Fabio Tranchitella Thu, 24 Jan 2008 10:12:02 +0100 + +dovecot (1:1.0.10-1) unstable; urgency=high + + * New upstream release, fixes a security bug. + * debian/patches/exec_check_for_none.dpatch: updated. + + -- Fabio Tranchitella Sun, 30 Dec 2007 10:29:26 +0100 + +dovecot (1:1.0.9-1) unstable; urgency=low + + * New upstream release. + * debian/control: added the Vcs-Svn and Vcs-Browser fields. + + -- Fabio Tranchitella Wed, 12 Dec 2007 08:10:11 +0100 + +dovecot (1:1.0.8-2) unstable; urgency=low + + * Provides a dovecot-dev package, thanks to Josh Triplett for providing a + patch. (Closes: #444812) + + -- Fabio Tranchitella Tue, 04 Dec 2007 09:22:59 +0100 + +dovecot (1:1.0.8-1) unstable; urgency=low + + * New upstream release. + * debian/patches/unsupported-sasl-mech.dpatch: merged with upstream. + + -- Fabio Tranchitella Thu, 29 Nov 2007 13:36:39 +0100 + +dovecot (1:1.0.7-3) unstable; urgency=low + + * debian/patches/dovecot-ssl.dpatch: provide mechanism to discover if ssl + client certificate is verified, patch from Stephen Gran. (Closes: #446555) + * debian/patches/pam-error-information.dpatch: fill auth information in pam + error, patch backported from upstream RCS. (Closes: #439246) + * debian/patches/unsupported-sasl-mech.dpatch: should use NO (not BAD) for + unsupported SASL mech, patch backported from upstream RCS. (Closes: #449324) + + -- Fabio Tranchitella Wed, 14 Nov 2007 21:33:55 +0100 + +dovecot (1:1.0.7-2) unstable; urgency=low + + * debian/dovecot-common.postinst: + + don't fail if dovecot-ldap.conf or dovecot-sql.conf are removed; thanks + to Mathias Gug. (Closes: #448401) + + fix permissions of /var/run/dovecot and /var/run/dovecot/login. + (Closes: #446051) + + -- Fabio Tranchitella Sun, 04 Nov 2007 10:06:06 +0100 + +dovecot (1:1.0.7-1) unstable; urgency=low + + * New upstream release. + * debian/rules: remove drac.so in the clean target. (Closes: #442548) + * debian/dovecot-common.init: implemented the reload action. + (Closes: #441032) + * Update protocols option in configuration when installing/removing + -imapd/-pop3d packages. Thanks to Soren Hansen and Mathias Gug from + Ubuntu for providing a patch. (Closes: #447201) + + -- Fabio Tranchitella Fri, 02 Nov 2007 23:06:17 +0100 + +dovecot (1:1.0.5-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Mon, 10 Sep 2007 09:25:59 +0200 + +dovecot (1:1.0.3-3) unstable; urgency=low + + * debian/dovecot-common.init: don't use the init script name to locate the + configuration file because it is not reliable. If you really want to start + multiple servers, just copy the init script and use the optional default + file to override the variables. (Closes: #437228) + + -- Fabio Tranchitella Thu, 16 Aug 2007 09:17:01 +0200 + +dovecot (1:1.0.3-2) unstable; urgency=low + + * debian/rules: removed the --with-notify=inotify switch, it should be + detected automatically. + + -- Fabio Tranchitella Thu, 09 Aug 2007 09:39:50 +0200 + +dovecot (1:1.0.3-1) unstable; urgency=low + + * New upstream release. (Closes: #434038, #432601) + * This release doesn't build dbox support out-of-the-box. (Closes: #431615) + * dovecot-sieve: updated to the last hg's tip. (Closes: #434079) + * debian/dovecot-*.README.Debian: don't call /etc/init.d scripts directly. + (Closes: #431991) + * debian/dovecot-common.init: updated with patches from Tom Metro, thanks! + (Closes: #426480, #422674) + * debian/dovecot-common.postinst: fixed a missing variable DOMAINNAME. + (Closes: #425917) + * debian/rules: moved the init script to the level 24, after the ntpdate + one. (Closes: #432723) + * debian/patches/00list: added dovecot-drac, again. (Closes: #353039) + * debian/rules: build with inotify and epoll support. (Closes: #419281) + * debian/dovecot.1: added a simple manpage for dovecot. (Closes: #426702) + * debian/copyright: added copyright exceptions as suggested by Timo. + + -- Fabio Tranchitella Sat, 04 Aug 2007 20:11:36 +0200 + +dovecot (1:1.0.0-1) unstable; urgency=low + + * New upstream release, the final 1.0.0. Bumped epoch, because we used the + wrong version scheming in the past and I think it is worth to do so now + that 1.0.0 has been released. + * debian/watch: updated. + * Rebuilt with a new glibc in unstable, now we have inotify support. + (Closes: #395306) + + -- Fabio Tranchitella Mon, 16 Apr 2007 08:42:32 +0200 + +dovecot (1.0.rc31-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Mon, 09 Apr 2007 11:55:45 +0200 + +dovecot (1.0.rc30-2) unstable; urgency=low + + * debian/dovecot-common.init: check if /etc/inetd.conf exists before + calling sed on it. (Closes: #417299) + + -- Fabio Tranchitella Sun, 08 Apr 2007 16:58:30 +0200 + +dovecot (1.0.rc30-1) unstable; urgency=low + + * New upstream release. + * + + -- Fabio Tranchitella Sat, 07 Apr 2007 11:17:45 +0200 + +dovecot (1.0.rc29-1) unstable; urgency=low + + * New upstream release. + * debian/rules: ship convert-tool in dovecot-common. (Closes: #416909) + + -- Fabio Tranchitella Sat, 31 Mar 2007 14:15:39 +0200 + +dovecot (1.0.rc28-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Sun, 25 Mar 2007 14:02:28 +0200 + +dovecot (1.0.rc27-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Wed, 14 Mar 2007 14:39:04 +0100 + +dovecot (1.0.rc26-4) unstable; urgency=low + + * debian/dovecot-common.postinst: fixed a typo. (Closes: #414672) + + -- Fabio Tranchitella Tue, 13 Mar 2007 08:45:47 +0100 + +dovecot (1.0.rc26-3) unstable; urgency=low + + * debian/control: depends on ucf (>= 2.0020). (Closes: #414032) + * debian/dovecot-common.postinst: handles better chmod/chown on package + upgrade. (Closes: #414257) + + -- Fabio Tranchitella Mon, 12 Mar 2007 10:03:34 +0100 + +dovecot (1.0.rc26-2) unstable; urgency=low + + * debian/control: + + dovecot-{imapd,pop3d}: depends on the same-source dovecot-common. + (Closes: #414032) + * debian/rules: fixed permission for dovecot.conf. (Closes: #413995) + + -- Fabio Tranchitella Fri, 9 Mar 2007 12:57:50 +0100 + +dovecot (1.0.rc26-1) unstable; urgency=low + + * New upstream release. + * Add support for ucf, thanks to Vincent Danjean for providing a full patch. + (Closes: #413081) + * debian/dovecot-common.init: create /var/run directories at start-up time. + (Closes: #376143) + + -- Fabio Tranchitella Wed, 7 Mar 2007 11:26:56 +0100 + +dovecot (1.0.rc25-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Fri, 2 Mar 2007 13:44:44 +0100 + +dovecot (1.0.rc24-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Fri, 23 Feb 2007 15:47:30 +0100 + +dovecot (1.0.rc23-1) unstable; urgency=low + + * New upstream release. + * debian/patches/documentation.dpatch, debian/patches/xfs_quotas.dpatch: + removed, applied upstream. + + -- Fabio Tranchitella Wed, 21 Feb 2007 09:34:44 +0100 + +dovecot (1.0.rc22-2) UNRELEASED; urgency=low + + * debian/watch: added. + + -- Fabio Tranchitella Fri, 9 Feb 2007 12:15:34 +0100 + +dovecot (1.0.rc22-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Wed, 7 Feb 2007 09:38:34 +0100 + +dovecot (1.0.rc21-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Mon, 5 Feb 2007 16:23:33 +0100 + +dovecot (1.0.rc19-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Wed, 24 Jan 2007 00:03:38 +0100 + +dovecot (1.0.rc18-2) unstable; urgency=low + + * dovecot-sieve: updated to cvs version 1.0.1. + * debian/README.Debian: updated for the new setting mail_location, which + substitutes the old default_mail_env. (Closes: #408025) + + -- Fabio Tranchitella Tue, 23 Jan 2007 10:15:36 +0100 + +dovecot (1.0.rc18-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Mon, 22 Jan 2007 18:15:02 +0100 + +dovecot (1.0.rc17-1) unstable; urgency=low + + * New upstream release. + * Updated dovecot-sieve from CVS. + + -- Fabio Tranchitella Fri, 12 Jan 2007 09:42:47 +0100 + +dovecot (1.0.rc15-2) unstable; urgency=medium + + * debian/dovecot-common.README.Debian: updated details about raw logging; + thanks Chris Moore for providing a patch. (Closes: #400689) + * debian/patches/dovecot-example.dpatch: added a missing slash for an + absolute path. (Closes: #400830) + * debian/patches/dovecotpw.dpatch: applied patched to fix argument parsing + on some architectures. (Closes: #402075) + + -- Fabio Tranchitella Mon, 18 Dec 2006 18:34:31 +0100 + +dovecot (1.0.rc15-1) unstable; urgency=medium + + * New upstream release. + * Fixes a security bug: Off-by-one buffer overflow with mmap_disable=yes. + (See: http://www.dovecot.org/list/dovecot-news/2006-November/000023.html) + + -- Fabio Tranchitella Mon, 20 Nov 2006 12:47:39 +0100 + +dovecot (1.0.rc14-1) unstable; urgency=medium + + * New upstream release. + + -- Fabio Tranchitella Thu, 16 Nov 2006 09:37:38 +0100 + +dovecot (1.0.rc13-1) unstable; urgency=medium + + * New upstream release. + * debian/rules: + + preserve upstream config.guess and config.sub. (Closes: #397404) + + really clean dovecot-sieve/src on clean target. (Closes: #397407) + * debian/control: added build-conflict with automake1.4. (Closes: #397409) + * dovecot-sieve/src/Makefile.am: move sieve plug-ins under + usr/lib/dovecot/lda/modules; thanks to Chris Vanden Berghe for pointing + this out. + + -- Fabio Tranchitella Tue, 7 Nov 2006 09:26:56 +0100 + +dovecot (1.0.rc12-1) unstable; urgency=low + + * New upstream release. + + -- Fabio Tranchitella Sun, 5 Nov 2006 16:52:52 +0100 + +dovecot (1.0.rc10-3) unstable; urgency=medium + + * debian/rules: fixed two typos in the configure call. (Closes: #395016) + * Included dovecot-sieve plug-in from CVS. (Closes: #394885) + * Urgency medium: we are near the freeze, and this release must be part + of etch. + + -- Fabio Tranchitella Tue, 31 Oct 2006 06:30:45 +0000 + +dovecot (1.0.rc10-2) unstable; urgency=low + + * debian/patches/dovecot-example.dpatch: commented out a close brace. + (Closes: #394785) + + -- Fabio Tranchitella Mon, 23 Oct 2006 08:17:13 +0000 + +dovecot (1.0.rc10-1) unstable; urgency=low + + * New upstream release. (Closes: #393004) + * debian/patches/dovecot-example.dpatch: + + added specific comments to the mail_extra_groups option. + (Closes: #383453) + + removed duplicated LDA section. (Closes: #391632) + * debian/dovecot.8: fixed a layout error. (Closes: #393080) + * debian/dovecot-common.init: added LSB headers. + * Switched to the upstream dovecot deliver (LDA). + * debian/patches/quota_v2.dpatch: added, thanks to Jonas Smedegaard. + (Closes: #377563) + + -- Fabio Tranchitella Sun, 22 Oct 2006 08:55:16 +0000 + +dovecot (1.0.rc7-1) unstable; urgency=low + + * New upstream release. (Closes: #377840, #385101) + * debian/patches/dovecot-example.dpatch: set a default value for + pop3_uidl_format. (Closes: #383883) + + -- Fabio Tranchitella Tue, 29 Aug 2006 10:38:17 +0200 + +dovecot (1.0.rc6-1) unstable; urgency=low + + * New upstream release: + + Fixed imap segfaults on small mbox files (2 bytes). (Closes: #377840) + + Fixed a known bug in dovecot's IDLE handler. (Closes: #351828) + + Added support for quota2. (Closes: #377563) + * debian/control: converted build-depends on linux-kernel-headers to + build-conflicts to help the GNU/kFreeBSD port. (Closes: #377479) + * debian/control: changed maintainer to "Dovecot Maintainers"; no changes + to the email addresses. + + -- Fabio Tranchitella Tue, 15 Aug 2006 10:58:57 +0200 + +dovecot (1.0.rc2-2) unstable; urgency=low + + * patched the quota plugin to fix a missing symbol (Closes: #377018) + + -- Jaldhar H. Vyas Fri, 7 Jul 2006 10:50:04 -0400 + +dovecot (1.0.rc2-1) unstable; urgency=high + + * New upstream release + * Update dovecot-lda to the latest version (05132006) + * IPv6 with SSL/TLS should work now. (Closes: #374783) + * go back to using poll instead of epoll. (Closes: #376222) + + -- Jaldhar H. Vyas Thu, 6 Jul 2006 00:47:56 -0400 + +dovecot (1.0.rc1-1) unstable; urgency=low + + * New upstream release. + * Add a build-dependency on linux-kernel-headers for the xfs quotas + stuff. Make it higher than the version in sarge because sarges xfs + includes are too old. If any knowledgeable person would like to + give me a patch for this, please do. (Closes: #374793) + + -- Jaldhar H. Vyas Wed, 28 Jun 2006 11:42:07 -0400 + +dovecot (1.0.beta9-1) unstable; urgency=low + + * New upstream release + * Added XFS quota support. Thanks Pawel Jarosz + (Closes: #373936) + + -- Jaldhar H. Vyas Mon, 19 Jun 2006 16:55:20 -0400 + +dovecot (1.0.beta8-4) unstable; urgency=high + + * Unfortunately, the patch in the last version broke the mysql module. + Fixed thanks to Martin Pitt. (Closes: #369359, #373227) + + -- Jaldhar H. Vyas Sun, 11 Jun 2006 16:27:43 -0400 + +dovecot (1.0.beta8-3) unstable; urgency=high + + * [SECURITY] SQL injection could occur in the postgresql module with + certain client character encodings. (See CVE-2006-2314) + Used the patch from upstream and Martin Pitt . + Thanks Martin. (Closes: #369359) + + -- Jaldhar H. Vyas Sun, 11 Jun 2006 15:33:55 -0400 + +dovecot (1.0.beta8-2) unstable; urgency=high + + * Don't chown/chmod ssl certificate unless we created it. + (Closes: #364766) + * Upstream fixed the crash if passwd-file had entries without passwords. + (Closes: #361536) + * fixed up the last versions changelog to better describe the security + problem which was fixed there. + + -- Jaldhar H. Vyas Sun, 21 May 2006 13:16:17 -0400 + +dovecot (1.0.beta8-1) unstable; urgency=high + + * New upstream release. + * [SECURITY] Fixes a directory traversal vulnerability. + (see: http://www.dovecot.org/list/dovecot-news/2006-May/000006.html + and CVE-2006-2414) + * Set urgency to high: this version fixes a security bug + * Standards-Version: 3.7.2, no changes needed. + + -- Fabio Tranchitella Sat, 13 May 2006 22:46:16 +0200 + +dovecot (1.0.beta7-1) unstable; urgency=low + + * New upstream version. + * Added sqlite support. + + -- Jaldhar H. Vyas Wed, 12 Apr 2006 23:25:41 -0400 + +dovecot (1.0.beta5-1) unstable; urgency=low + + * New upstream version. Also updated dovecot-lda from CVS. + * debian/control. Added build-depends on flex to prevent FTBFS. + + -- Jaldhar H. Vyas Thu, 6 Apr 2006 16:22:46 -0400 + +dovecot (1.0.beta3-3) unstable; urgency=low + + * Compile against the newer mysql library. (Closes: #356729) + + -- Fabio Tranchitella Wed, 22 Mar 2006 13:43:04 +0000 + +dovecot (1.0.beta3-2) unstable; urgency=low + + [Fabio Tranchitella] + * debian/control: added build-depends on byacc. + * debian/rules: removed --with-vpopmail option, because libvpopmail-dev + is in contrib and we don't wanto to have dovecot build-depends on it. + * debian/patches/dovecot-example.dpatch: added two small commented + block of configuration for dovecot-lda. + + -- Fabio Tranchitella Sun, 26 Feb 2006 20:59:06 +0000 + +dovecot (1.0.beta3-1) unstable; urgency=high + + [Fabio Tranchitella] + * New upstream release, which fixes two security related bugs. + CVE-2006-0730 (Closes: #353341) + * Included dovecot-lda (ver. 20060209). (Closes: #353307, #347348, #333962) + + [ Jaldhar H. Vyas ] + * Removed the code for upgrading impad.pem. This might bite if you if + you try and upgrade a woody version of dovecot to this one. So + don't do that. (Closes: #337715) + * dovecot-imapd,dovecot-pop3d: depend on dovecot-common >= 1.0beta3-1 + as the way SSL parameters are generated has changed. (Closes: #353404) + + -- Jaldhar H. Vyas Fri, 17 Feb 2006 22:06:21 -0500 + +dovecot (1.0.beta2-1) unstable; urgency=low + + [Fabio Tranchitella] + * New upstream release. + * debian/rules: compile with vpopmail support. (Closes: #347838) + * debian/patches: removed zlib patch (merged with upstream). + + -- Fabio Tranchitella Thu, 2 Feb 2006 21:38:32 +0000 + +dovecot (1.0.alpha5-1) unstable; urgency=low + + [Fabio Tranchitella] + * New upstream release. + * Compile dovecot with Kerberos support. (Closes: #338384) + * Fixed a small typo in mbox specification. (Closes: #339789) + * Added man page for maildirmake.dovecot, thanks to Henry Precheur. + (Closes: #340498) + * Use /usr/lib/dovecot/modules as basedir for dynamic modules. + Upstream suggests /usr/lib/dovecot, but we already use it as + libexec directory. + + -- Fabio Tranchitella Wed, 21 Dec 2005 13:44:38 +0000 + +dovecot (1.0.alpha4-1) unstable; urgency=low + + [Jaldhar H. Vyas] + * New upstream version. + * Made sure the default dovecot.conf includes mail_extra_groups=mail + (Closes: #336476) somehow this edit got lost at some point. + * use ISO8601 date format as default value for log_timestamp in + /etc/dovecot/dovecot.conf (Closes: #333059) + * stop shipping {arch} directories in source (Closes: #334646) + * Include plugin for compressed mboxen (Closes: #332384) + * updated NEWS.Debian to warn users that the dovecot.conf syntax has + changed (Closes: #334209) + * Remember, remember, the 5th of November. + + -- Jaldhar H. Vyas Sat, 5 Nov 2005 23:19:19 -0500 + +dovecot (1.0.alpha3-2) unstable; urgency=low + + [Jaldhar H. Vyas] + * dovecot-common: When creating the dovecot user in the postinst, + the --ingroup option to adduser to add dovecot to group + mail isn't used anymore. + (Closes: #330960, #331106) + * commented out userdb passdb from default configuration. Most + people won't need that. (Closes: #330978) + + -- Jaldhar H. Vyas Thu, 6 Oct 2005 14:25:33 -0400 + +dovecot (1.0.alpha3-1) unstable; urgency=low + + [ Fabio Tranchitella ] + * New upstream release (dovecot-1.0.alpha.3) + * debian/patches/ipv6_v6only.dpatch: removed, upstream accepted it. + * debian/dovecot-common.postinst: removed bashisms. + * debian/dovecot-common.postinst: add dovecot user to group mail. + (Closes: #323921) + * debian/control: removed conflicts with imap-server and pop3-server, + added replaces instead. (Closes: #324480) + [ Jaldhar H. Vyas ] + * No longer crashes when using LDAP as userdb/passdb (Closes: #320388) + + -- Jaldhar H. Vyas Mon, 26 Sep 2005 01:42:09 -0400 + +dovecot (0.99.20050712-2) unstable; urgency=low + + * Fabio Tranchitella + + debian/control: dovecot-common has to depend on adduser. + + debian/patches/documentation.dpatch: some cosmetic fixes about mysql + backend. + * Jaldhar H. Vyas + + debian/control: tighten dovecot-imapd and dovecot-pop3d's dependency on + dovecot-common (Closes: #319465) + + debian/patches/dovecot-example.dpatch: some more fixes to default + configuration. (Closes: #319413, #319941) + + -- Jaldhar H. Vyas Fri, 29 Jul 2005 15:37:52 -0400 + +dovecot (0.99.20050712-1) unstable; urgency=low + + * Fabio Tranchitella + + New upstream version (dovecot-stable, last update 20050712). + (Closes: #312893) + + debian/control: Standards-Version: 3.6.2 (no changes needed). + + debian/patches/dovecot-sql.dpatch: use the right path for mysql socket. + (Closes: #298874) + * Jaldhar H. Vyas + + Removed dovecot package as it was just a woody->sarge transitional + pseudo-package. + + Apply patch to debian/dovecot-common.init to help when manually + starting dovecot. Thanks Roland Stigge. (Closes: #309679) + + Apply patch to src/lib/network.c to support IPV6_V6ONLY. Thanks + Marco D'Itri. (Closes: #308652) + + depend on the latest postgresql library. + + -- Jaldhar H. Vyas Wed, 20 Jul 2005 06:30:37 -0400 + +dovecot (0.99.14-1) unstable; urgency=low + + * New upstream version. + * dovecot-common: another postinst regexp fix for SSL cert/key files. + (Closes: #294989) + + -- Jaldhar H. Vyas Sat, 12 Feb 2005 21:34:33 -0500 + +dovecot (0.99.13-6) unstable; urgency=high + + * dovecot-common: *sigh* another init script fix. Hopefully we now + fully deal with dovecot being run from inetd. Thanks again to Magnus + Holmgren. (Closes: #293348) + * High again so -5 doesn't get into sarge. + + -- Jaldhar H. Vyas Mon, 7 Feb 2005 02:58:30 -0500 + +dovecot (0.99.13-5) unstable; urgency=high + + * dovecot-common: typo in postinst resulted in incorrect generation of + keys for first-time installers. Hence urgency high. + * dovecot-common: In init script, make extra check to make sure an + IMAP or POP3 server called from inetd is dovecot and not some other + random inferior product. (Closes: #293348) + + -- Jaldhar H. Vyas Sat, 5 Feb 2005 13:56:31 -0500 + +dovecot (0.99.13-4) unstable; urgency=low + + * build depend on libmysqlclient12 + * dovecot-common: Allow STARTTLS to work when dovecot is run from inetd + Thanks Magnus Holmgren (Closes: #290985) + * dovecot-common: let init script exit if dovecot is being run from inetd + Thanks Magnus Holmgren (Closes: #292195) + * dovecot-common: fix a number of problems in postinst + + fails if /etc/ssl/certs or /etc/ssl/private doesn't exist + + certs cannot be generated and upgrade fails if openssl is not + configured. Fail more gracefully if this is the case. + + read the name and path for the cert from dovecot.conf instead of + hardcoding it. + Thanks Frederic Pauget (Closes: #292344) + + -- Jaldhar H. Vyas Sun, 30 Jan 2005 15:20:03 -0500 + +dovecot (0.99.13-3) unstable; urgency=high + + * Oops -2 had to be urgency=high so -1 doesn't get into sarge. + + -- Jaldhar H. Vyas Sat, 8 Jan 2005 12:11:38 -0500 + +dovecot (0.99.13-2) unstable; urgency=low + + * dovecot-imapd, dovecot-pop3d: It occurred to me that the effects of + fixing #288391 will cause confusion in the minds of new installers so I + should add a warning in README.Debian and NEWS.Debian in a vain + effort to stave off swarms of bug reports. (Vain, because no one + actually reads documentation anyway.) + + -- Jaldhar H. Vyas Sat, 8 Jan 2005 11:29:59 -0500 + +dovecot (0.99.13-1) unstable; urgency=high + + * New upstream version. + * dovecot-imapd, dovecot-pop3d: No longer mess with dovecot.conf in postinst + (Closes: #288391) + * urgency high due to #288391 being a release-critical bug. + + -- Jaldhar H. Vyas Fri, 7 Jan 2005 17:37:08 -0500 + +dovecot (0.99.12-1) unstable; urgency=low + + * New upstream version. (Yes I know 0.99.13 is just around the corner.) + * SASL is re-enabled so this bug ("Dovecot seems not to require SASL") + is no longer valid (Closes: #272093) + * Configuration files moved to /etc/dovecot (Closes: #276183) + * Permissions on /var/run/dovecot and /var/run/dovecot/login no longer + give warnings. (Closes: #283996) + * SSL certificate is world readable (Closes: #277114). + * Thanks to Jan Buren, much extra documentation has been added to + /usr/share/doc/dovecot-common/README.Debian + * Lintian overrides added. + * Happy new year to all you Gregorians + + -- Jaldhar H. Vyas Fri, 31 Dec 2004 15:55:07 -0500 + +dovecot (0.99.11-3) unstable; urgency=medium + + * applied dovecot-large-header-fix patch to prevent 100% CPU + utilization when dealing with really large headers. + (Closes: #271458) + + -- Jaldhar H. Vyas Mon, 13 Sep 2004 20:20:23 -0400 + +dovecot (0.99.11-2) unstable; urgency=low + + * Eliminated duplicate stanza in dovecot.conf (Closes: #270181) + * Reapplied CRAM-MD5 patch. + + -- Jaldhar H. Vyas Mon, 6 Sep 2004 01:24:53 -0400 + +dovecot (0.99.11-1) unstable; urgency=low + + * New upstream release. + * patch to give bug reporting address in configure.ac. + Thanks Matthias Andree. + + -- Jaldhar H. Vyas Sat, 4 Sep 2004 14:24:57 -0400 + +dovecot (0.99.10.9-2) unstable; urgency=low + + * screw mipsel. + * Added PAM_RHOST patch. Thanks Dean Gaudet. (Closes: #264712) + * Added CRAM-MD5 patch. Thanks Joshua Goodall. + * Added unexpected EOF patch from Timo. + + -- Jaldhar H. Vyas Tue, 17 Aug 2004 01:13:20 -0400 + +dovecot (0.99.10.9-1) unstable; urgency=low + + * New upstream release. + + -- Jaldhar H. Vyas Mon, 2 Aug 2004 18:56:02 -0400 + +dovecot (0.99.10.8-1) unstable; urgency=low + + * New upstream release. + + -- Jaldhar H. Vyas Fri, 30 Jul 2004 08:17:51 -0400 + +dovecot (0.99.10.7-1) unstable; urgency=low + + * New upstream release. + + -- Jaldhar H. Vyas Wed, 14 Jul 2004 07:29:49 -0400 + +dovecot (0.99.10.6-3) unstable; urgency=low + + * Patched so dovecot follows symlinks to directories again. + (Closes: #256061) + * Changed the priority of init script so it is run after postgresql + (Closes: #256068) + + -- Jaldhar H. Vyas Thu, 24 Jun 2004 23:57:50 -0400 + +dovecot (0.99.10.6-2) unstable; urgency=high + + * I needed to enable one more parameter in the configuration in order + to get dot-locking working. Hence this should still be high urgency. + (Really Closes: #185335) + + -- Jaldhar H. Vyas Mon, 21 Jun 2004 20:02:29 -0400 + +dovecot (0.99.10.6-1) unstable; urgency=high + + * New upstream version. + + finally fixes dot-locking so I think it deserves high priority for + sarge. (Closes: #185335) + * dovecot: fixed a typo in description (Closes: #254415) + * dovecot-common: man page for dovecot added. Thanks Kai Hendry. + (Closes: #253482) + + -- Jaldhar H. Vyas Sat, 19 Jun 2004 23:18:39 -0400 + +dovecot (0.99.10.5-4) unstable; urgency=high + + * Crap, typo in dovecot-common.postinst sorry. This should only + affect new installs though. + + -- Jaldhar H. Vyas Fri, 11 Jun 2004 22:30:41 -0400 + +dovecot (0.99.10.5-3) unstable; urgency=high + + * SECURITY: set permissions on config files to 0600 to prevent + disclosure of sensitive information to local users. (Closes: #253760) + * SECURITY: Tightened permissions on generated SSL certificate. + (Closes: #253833) + * Made a note that dovecot-openssl.conf is not needed on Debian + because we generate a certificate in dovecot-commons' postinst. + (Closes: #253774) + * Added maildir-autocreate patch. + + -- Jaldhar H. Vyas Fri, 11 Jun 2004 09:12:07 -0400 + +dovecot (0.99.10.5-2) unstable; urgency=low + + * Added maildir-stat patch. + + -- Jaldhar H. Vyas Thu, 10 Jun 2004 14:39:28 -0400 + +dovecot (0.99.10.5-1) unstable; urgency=low + + * New upstream version. + * Enabled mysql support. + + -- Jaldhar H. Vyas Thu, 27 May 2004 13:21:42 -0400 + +dovecot (0.99.10.4-5) unstable; urgency=high + + * Switched to using openssl as dovecot segfaults with gnutls7 and is + not compatible with gnutls10 (Closes: #244570) + + -- Jaldhar H. Vyas Mon, 19 Apr 2004 22:45:21 -0400 + +dovecot (0.99.10.4-4) unstable; urgency=low + + * Added a patch to src/auth/db-pgsql.c from Zsolt VARGA. + + -- Jaldhar H. Vyas Tue, 30 Mar 2004 12:49:31 -0500 + +dovecot (0.99.10.4-3) unstable; urgency=high + + * dovecot-common: Fix postinst to no longer delete /etc/pam.d/imap + (Closes: #232832) + + -- Jaldhar H. Vyas Mon, 15 Mar 2004 10:27:52 -0500 + +dovecot (0.99.10.4-2) unstable; urgency=low + + * dovecot-common: now replaces: dovecot for smoother upgrades. + (Closes: #223666) + + -- Jaldhar H. Vyas Thu, 11 Dec 2003 09:18:12 -0500 + +dovecot (0.99.10.4-1) unstable; urgency=low + + * New upstream version + + This fixes the corruption of .subscriptions files with folders in + maildir format. (Closes: #222272) + * Some extra information included in dovecot-common.README.Debian. + (Closes: #221106) There should probably be more so if you have ideas + let me know. + + -- Jaldhar H. Vyas Mon, 1 Dec 2003 23:41:00 -0500 + +dovecot (0.99.10.2-1) unstable; urgency=low + + * New upstream version. + + -- Jaldhar H. Vyas Tue, 11 Nov 2003 17:21:45 -0500 + +dovecot (0.99.10-11) unstable; urgency=low + + * Build depend on gnutls 7 instead of 5 (Closes: #219523) + + -- Jaldhar H. Vyas Thu, 6 Nov 2003 22:25:00 -0500 + +dovecot (0.99.10-10) unstable; urgency=low + + * maildirmake.dovecot will now let you create maildirs whose names + have spaces in them and chown them to a specified user. Thanks Paul + Slootman (Closes: #219168) + + -- Jaldhar H. Vyas Tue, 4 Nov 2003 20:00:25 +0000 + +dovecot (0.99.10-9) unstable; urgency=low + + * Don't use SASL2 as upstream says the support is broken. + + -- Jaldhar H. Vyas Tue, 23 Sep 2003 16:27:11 +0000 + +dovecot (0.99.10-8) unstable; urgency=low + + * Patched so suid works on 2.6 kernels. Thanks Peter Gervai. + (Closes: #211420) + + -- Jaldhar H. Vyas Wed, 17 Sep 2003 17:59:10 +0000 + +dovecot (0.99.10-7) unstable; urgency=low + + * Yet another init script fix. It should be ok now. Thanks once again + to Alexis Iglauer. + + -- Jaldhar H. Vyas Wed, 27 Aug 2003 10:56:04 -0400 + +dovecot (0.99.10-6) unstable; urgency=low + + * fix some errors in init script (closes: #207464) + Thanks to Adam Lackorzynski and Alexis Iglauer. + + -- Jaldhar H. Vyas Wed, 27 Aug 2003 09:02:23 -0400 + +dovecot (0.99.10-5) unstable; urgency=high + + * dovecot-pop3d, dovecot-imapd: make sure init script doesn't + attempt to start the daemons if unconfigured thus preventing + segfault on startup. (Closes: #206992, #207140) + + -- Jaldhar H. Vyas Mon, 25 Aug 2003 16:40:53 -0400 + +dovecot (0.99.10-4) unstable; urgency=low + + * Updated PAM configuration to the new scheme and added appropriate + dependency. + + -- Jaldhar H. Vyas Fri, 22 Aug 2003 12:54:54 -0400 + +dovecot (0.99.10-3) unstable; urgency=low + + * dovecot-pop3d: Patch for proper PAM service name. + * dovecot-imapd, dovecot-pop3d: Make sure there is an appropriate + entry in the protocol = line in /etc/dovecot.conf so the service + will start up without errors. (Closes: #204213) + + -- Jaldhar H. Vyas Thu, 21 Aug 2003 13:47:00 -0400 + +dovecot (0.99.10-2.1) unstable; urgency=low + + * Non-maintainer upload at request of maintainer. + * Fix segfault on alpha caused by time_t size. Closes: #203892. + * Fix segfault when user's home directory is left empty. + + -- Scott James Remnant Wed, 6 Aug 2003 01:47:16 +0100 + +dovecot (0.99.10-2) unstable; urgency=low + + * corrected paths to example and configuration files in sample config + (Closes: #199740) + * Added postgresql support. + + -- Jaldhar H. Vyas Thu, 3 Jul 2003 16:45:49 -0400 + +dovecot (0.99.10-1) unstable; urgency=low + + * New upstream release. + * PAM service name has changed to dovecot (for IMAP and POP3.) I've included + code to move /etc/pam.d/imap to /etc/pam.d/dovecot but if things suddenly + stop working, this is the first thing to check. + + -- Jaldhar H. Vyas Thu, 26 Jun 2003 22:31:07 -0400 + +dovecot (0.99.10-0.rc2) unstable; urgency=low + + * New upstream release. Fixes broken imaps support. + * Typo in configure options that broke LDAP support on woody corrected. + * Only start /usr/sbin/dovecot if either the IMAP or POP3 servers are + installed. (Closes: #192066) + + -- Jaldhar H. Vyas Mon, 23 Jun 2003 23:26:04 -0400 + +dovecot (0.99.9.1-1) unstable; urgency=low + + * New upstream release. + + -- Jaldhar H. Vyas Sun, 4 May 2003 21:49:55 -0400 + +dovecot (0.99.9-1) unstable; urgency=low + + * New upstream release. + * The IMAP and POP3 servers have been split into separate package so you + don't have to install both. There is also a dovecot-common package + for the parts they share. The dovecot package is now a dummy just for + transitioning to this new scheme. (Closes: #187826) + * Allow chmod in maildirmake.dovecot to fail gracefully (Closes: #191244) + + -- Jaldhar H. Vyas Wed, 30 Apr 2003 08:53:46 -0400 + +dovecot (0.99.8.1-4) unstable; urgency=low + + * Added a build-depends on libsasl-dev (Closes: #187516) + * Enabled pop3 service. However it is still turned off in the config file by + default so as to not surprise anyone who thought they only had an IMAP + server. + * Consequently, changed "IMAP server" in descriptions etc. to "mail server". + * only skip key generation if both /etc/ssl/certs/dovecot.pem and + /etc/ssl/private/dovecot.pem exist. (Closes: #187638) + * post-0.99.8.1 patch: Fix renaming subfolders with maildir. + * post-0.99.8.1 patch: Fix other maildir subfolder problems. + * post-0.99.8.1 patch: Fix partial body fetches. + * post-0.99.8.1 patch: Fix using LITERAL+APPEND. + + -- Jaldhar H. Vyas Sat, 5 Apr 2003 14:13:52 -0500 + +dovecot (0.99.8.1-3) unstable; urgency=low + + * Fixed bashism and perlism(!) in maildirmake.dovecot. Thanks Clint + Adams. (Closes: #185768) + * Enabled LDAP support. The configuration is in /etc/dovecot-ldap.conf + but is commented out. + * Happy new year to all Debian users still on the Julian calendar. + + -- Jaldhar H. Vyas Tue, 25 Mar 2003 09:25:37 -0500 + +dovecot (0.99.8.1-2) unstable; urgency=low + + * Make a separate check for /etc/ssl/private/imapd.pem in case we don't + have it. (Closes: #185334) + * Move check for dovecot user and creation code to postinsts' configure + phase. (Closes: #185333) + + -- Jaldhar H. Vyas Wed, 19 Mar 2003 01:50:10 -0500 + +dovecot (0.99.8.1-1) unstable; urgency=low + + * new upstream release. (Closes: #184131) Does not include the POP3 + server or LDAP and SASL support for now so we can get into the archive + quickly. Fixes segfaults in imap-master (Closes: #184231) Fixes + faulty mail autodetection presets (Closes: #179625) + + -- Jaldhar H. Vyas Sun, 16 Mar 2003 15:47:54 -0500 + +dovecot (0.99.7-4) unstable; urgency=low + + * Fixed location of handlers. (Closes: #179273) + + -- Jaldhar H. Vyas Fri, 31 Jan 2003 18:37:23 -0500 + +dovecot (0.99.7-3) unstable; urgency=low + + * doh pam file should be called imap not dovecot (Closes: #179180) + + -- Jaldhar H. Vyas Fri, 31 Jan 2003 14:12:29 -0500 + +dovecot (0.99.7-2) unstable; urgency=low + + * Added two upstream patches to fix broken plain authentication and + building with vpopmail support (which is not enabled in the Debian + package yet.) + + -- Jaldhar H. Vyas Fri, 17 Jan 2003 14:24:22 -0500 + +dovecot (0.99.7-1) unstable; urgency=low + + * New upstream release. + * Syslog no longer fills up with entries when dovecot is misconfigured. + (Closes: #175507) + * startup is logged now. shutdowns were already logged. (Closes: #175509) + * dovecot will not remove your SSL certificates now. (Closes: #175282) + + -- Jaldhar H. Vyas Wed, 15 Jan 2003 00:58:23 -0500 + +dovecot (0.99.6rc2-1) unstable; urgency=low + + * New upstream release. + * Includes patch which stops dovecot from dying on alpha (Closes: #175577) + + -- Jaldhar H. Vyas Wed, 8 Jan 2003 14:22:09 -0500 + +dovecot (0.99.5-1) unstable; urgency=low + + * New upstream release with many fixes and improvements. + + -- Jaldhar H. Vyas Fri, 3 Jan 2003 00:53:26 -0500 + +dovecot (0.99.4-1) unstable; urgency=low + + * Initial Release. + + -- Jaldhar H. Vyas Wed, 18 Dec 2002 09:44:39 -0500 diff --git a/conf/conf.d/10-auth.conf b/conf/conf.d/10-auth.conf new file mode 100644 index 0000000..edd148c --- /dev/null +++ b/conf/conf.d/10-auth.conf @@ -0,0 +1,120 @@ +## +## Authentication processes +## + +# Enable LOGIN command and all other plaintext authentications even if +# SSL/TLS is not used (LOGINDISABLED capability). Note that if the remote IP +# matches the local IP (ie. you're connecting from the same computer), the +# connection is considered secure and plaintext authentication is allowed, +# unless ssl = required. +#auth_allow_cleartext = yes + +# Authentication cache size (e.g. 10M). 0 means it's disabled. Note that +# bsdauth, PAM and vpopmail require cache_key to be set for caching to be used. +#auth_cache_size = 0 +# Time to live for cached data. After TTL expires the cached record is no +# longer used, *except* if the main database lookup returns internal failure. +# We also try to handle password changes automatically: If user's previous +# authentication was successful, but this one wasn't, the cache isn't used. +# For now this works only with plaintext authentication. +#auth_cache_ttl = 1 hour +# TTL for negative hits (user not found, password mismatch). +# 0 disables caching them completely. +#auth_cache_negative_ttl = 1 hour + +# Space separated list of realms for SASL authentication mechanisms that need +# them. You can leave it empty if you don't want to support multiple realms. +# Many clients simply use the first one listed here, so keep the default realm +# first. +#auth_realms = +# +# Default realm/domain to use if none was specified. This is used for both +# SASL realms and appending @domain to username in plaintext logins. +#auth_default_domain = + +# List of allowed characters in username. If the user-given username contains +# a character not listed in here, the login automatically fails. This is just +# an extra check to make sure user can't exploit any potential quote escaping +# vulnerabilities with SQL/LDAP databases. If you want to allow all characters, +# set this value to empty. +#auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ + +# Username character translations before it's looked up from databases. The +# value contains series of from -> to characters. For example "#@/@" means +# that '#' and '/' characters are translated to '@'. +#auth_username_translation = + +# Username formatting before it's looked up from databases. +#auth_username_format = %{user|lower} +#auth_username_format = %{user|username|lower} + +# If you want to allow master users to log in by specifying the master +# username within the normal username string (ie. not using SASL mechanism's +# support for it), you can specify the separator character here. The format +# is then . UW-IMAP uses "*" as the +# separator, so that could be a good choice. +#auth_master_user_separator = + +# Username to use for users logging in with ANONYMOUS SASL mechanism +#auth_anonymous_username = anonymous + +# Host name to use in GSSAPI principal names. The default is to use the +# name returned by gethostname(). Use "$ALL" (with quotes) to allow all keytab +# entries. +#auth_gssapi_hostname = + +# Kerberos keytab to use for the GSSAPI mechanism. Will use the system +# default (usually /etc/krb5.keytab) if not specified. You may need to change +# the auth service to run as root to be able to read this file. +#auth_krb5_keytab = + +# Do NTLM and GSS-SPNEGO authentication using Samba's winbind daemon and +# ntlm_auth helper. +#auth_use_winbind = no + +# Path for Samba's ntlm_auth helper binary. +#auth_winbind_helper_path = /usr/bin/ntlm_auth + +# Time to delay before replying to failed authentications. +#auth_failure_delay = 2 secs + +# Require a valid SSL client certificate or the authentication fails. +#auth_ssl_require_client_cert = no + +# Take the username from client's SSL certificate, using +# X509_NAME_get_text_by_NID() which returns the subject's DN's +# CommonName. +#auth_ssl_username_from_cert = no + +# Space separated list of wanted authentication mechanisms: +# plain login digest-md5 cram-md5 ntlm anonymous gssapi +# gss-spnego xoauth2 oauthbearer +# NOTE: See also auth_allow_cleartext setting. +#auth_mechanisms = plain login + +## +## Password and user databases +## + +# +# Password database is used to verify user's password (and nothing more). +# You can have multiple passdbs and userdbs. This is useful if you want to +# allow both system users (/etc/passwd) and virtual users to login without +# duplicating the system users into virtual database. +# +# +# +# User database specifies where mails are located and what user/group IDs +# own them. For single-UID configuration use "static" userdb. +# +# + +#!include auth-deny.conf.ext +#!include auth-master.conf.ext +#!include auth-oauth2.conf.ext + +!include auth-system.conf.ext +#!include auth-sql.conf.ext +#!include auth-ldap.conf.ext +#!include auth-passwdfile.conf.ext +#!include auth-static.conf.ext diff --git a/conf/conf.d/10-logging.conf b/conf/conf.d/10-logging.conf new file mode 100644 index 0000000..359642f --- /dev/null +++ b/conf/conf.d/10-logging.conf @@ -0,0 +1,86 @@ +## +## Log destination. +## + +# Log file to use for error messages. "syslog" logs to syslog, +# /dev/stderr logs to stderr. +#log_path = syslog + +# Log file to use for informational messages. Defaults to log_path. +#info_log_path = +# Log file to use for debug messages. Defaults to info_log_path. +#debug_log_path = + +# Syslog facility to use if you're logging to syslog. Usually if you don't +# want to use "mail", you'll use local0..local7. Also other standard +# facilities are supported. +#syslog_facility = mail + +## +## Logging verbosity and debugging. +## + +# Log unsuccessful authentication attempts and the reasons why they failed. +#auth_verbose = yes + +# In case of password mismatches, log the attempted password. Valid values are +# no, plain and sha1. sha1 can be useful for detecting brute force password +# attempts vs. user simply trying the same password over and over again. +#auth_verbose_passwords = no + +# To chain multiple logging conditions you can use, +# log_debug=$SET:log_debug or category=xxx + +# Even more verbose logging for debugging purposes. Shows for example SQL +# queries. +#log_debug=category=auth +# +# In case of password mismatches, log the passwords and used scheme so the +# problem can be debugged. Enabling this also enables auth_debug. +#auth_debug_passwords = yes + +# Enable mail process debugging. This can help you figure out why Dovecot +# isn't finding your mails. +#log_debug=category=mail + +# Show protocol level SSL errors. +#log_debug=category=ssl + +# mail_log plugin provides more event logging for mail processes. +#mail_plugins { +# notify = yes +# mail_log = yes +#} +# Events to log. Also available: flag_change append +#mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename flag_change append +# Available fields: uid, box, msgid, from, subject, size, vsize, flags +# size and vsize are available only for expunge and copy events. +#mail_log_fields = uid box msgid size from subject vsize flags +# only log cached fields +#mail_log_cached_only = yes + +## +## Log formatting. +## + +# Prefix for each line written to log file. % codes are in strftime(3) +# format. +#log_timestamp = "%b %d %H:%M:%S " + +# Space-separated list of elements we want to log. The elements which have +# a non-empty variable value are joined together to form a comma-separated +# string. +#login_log_format_elements = user=<%{user}> method=%{mechanism} rip=%{remote_ip} lip=%{local_ip} mpid=%{mail_pid} %{secured} session=<%{session}> + +# Login log format. %{elements} contains login_log_format_elements string, %{message} contains +# the data we want to log. +#login_log_format = %{message}: %{elements} + +# Log prefix for mail processes. See +# https://doc.dovecot.org/latest/core/settings/variables.html#mail-service-user-variables +# for list of possible variables. +#mail_log_prefix = "%{service}(%{user})<%{process:pid}><%{session}>: " + +# Format to use for logging mail deliveries. See https://doc.dovecot.org/latest/core/summaries/settings.html#deliver_log_format +# for list of possible variables. +#deliver_log_format = msgid=%{msgid}: %{message} (subject=%{subject} from=%{from} size=%{size}) diff --git a/conf/conf.d/10-mail.conf b/conf/conf.d/10-mail.conf new file mode 100644 index 0000000..31938b4 --- /dev/null +++ b/conf/conf.d/10-mail.conf @@ -0,0 +1,410 @@ +## +## Mailbox locations and namespaces +## + +# Location for users' mailboxes. The default is empty, which means that Dovecot +# tries to find the mailboxes automatically. This won't work if the user +# doesn't yet have any mail, so you should explicitly tell Dovecot the full +# location. +# +# If you're using mbox, giving a path to the INBOX file (eg. /var/mail/%u) +# isn't enough. You'll also need to tell Dovecot where the other mailboxes are +# kept. This is called the "root mail directory", and it must be the first +# path given in the mail_location setting. +# +# There are a few special variables you can use, eg.: +# +# %{user} - username +# %{user|username} - user part in user@domain, same as %u if there's no domain +# %{user|domain} - domain part in user@domain, empty if there's no domain +# %{home} - home directory +# +# See https://doc.dovecot.org/latest/core/settings/variables.html for full list +# of variables. +# +# Example: +# mail_driver = maildir +# mail_path = ~/Maildir +# mail_inbox_path = ~/Maildir/.INBOX +# + +# Debian defaults +# Note that upstream considers mbox deprecated and strongly recommends +# against its use in production environments. See further information +# at +# https://doc.dovecot.org/2.4.1/core/config/mailbox/formats/mbox.html +mail_driver = mbox +mail_home = /home/%{user | username} +mail_path = %{home}/mail +mail_inbox_path = /var/mail/%{user} + +# If you need to set multiple mailbox locations or want to change default +# namespace settings, you can do it by defining namespace sections. +# +# You can have private, shared and public namespaces. Private namespaces +# are for user's personal mails. Shared namespaces are for accessing other +# users' mailboxes that have been shared. Public namespaces are for shared +# mailboxes that are managed by sysadmin. If you create any shared or public +# namespaces you'll typically want to enable ACL plugin also, otherwise all +# users can access all the shared mailboxes, assuming they have permissions +# on filesystem level to do so. +namespace inbox { + # Namespace type: private, shared or public + #type = private + + # Hierarchy separator to use. You should use the same separator for all + # namespaces or some clients get confused. '/' is usually a good one. + # The default however depends on the underlying mail storage format. + #separator = + + # Prefix required to access this namespace. This needs to be different for + # all namespaces. For example "Public/". + #prefix = + + # Physical location of the mailbox. This is in same format as + # mail location, which is also the default for it. + # mail_driver = + # mail_path = + # + # There can be only one INBOX, and this setting defines which namespace + # has it. + inbox = yes + + # If namespace is hidden, it's not advertised to clients via NAMESPACE + # extension. You'll most likely also want to set list=no. This is mostly + # useful when converting from another server with different namespaces which + # you want to deprecate but still keep working. For example you can create + # hidden namespaces with prefixes "~/mail/", "~%u/mail/" and "mail/". + #hidden = no + + # Show the mailboxes under this namespace with LIST command. This makes the + # namespace visible for clients that don't support NAMESPACE extension. + # "children" value lists child mailboxes, but hides the namespace prefix. + #list = yes + + # Namespace handles its own subscriptions. If set to "no", the parent + # namespace handles them (empty prefix should always have this as "yes") + #subscriptions = yes + + # See 15-mailboxes.conf for definitions of special mailboxes. +} + +# Example shared namespace configuration +#namespace shared { + #type = shared + #separator = / + + # Mailboxes are visible under "shared/user@domain/" + # $user, $domain and $username are expanded to the destination user. + #prefix = shared/$user/ + + # Mail location for other users' mailboxes. Note that %{variables} and ~/ + # expands to the logged in user's data. %{owner_user} and %{owner_home} + # destination user's data. + #mail_driver = maildir + #mail_path = %{owner_home}/Maildir + #mail_index_path = ~/Maildir/shared/%{owner_user} + + # Use the default namespace for saving subscriptions. + #subscriptions = no + + # List the shared/ namespace only if there are visible shared mailboxes. + #list = children +#} +# Should shared INBOX be visible as "shared/user" or "shared/user/INBOX"? +#mail_shared_explicit_inbox = no + +# System user and group used to access mails. If you use multiple, userdb +# can override these by returning uid or gid fields. You can use either numbers +# or names. +#mail_uid = +#mail_gid = + +# Group to enable temporarily for privileged operations. Currently this is +# used only with INBOX when either its initial creation or dotlocking fails. +# Typically this is set to "mail" to give access to /var/mail. +mail_privileged_group = mail + +# Grant access to these supplementary groups for mail processes. Typically +# these are used to set up access to shared mailboxes. Note that it may be +# dangerous to set these if users can create symlinks (e.g. if "mail" group is +# set here, ln -s /var/mail ~/mail/var could allow a user to delete others' +# mailboxes, or ln -s /secret/shared/box ~/mail/mybox would allow reading it). +#mail_access_groups = + +# Allow full filesystem access to clients. There's no access checks other than +# what the operating system does for the active UID/GID. It works with both +# maildir and mboxes, allowing you to prefix mailboxes names with eg. /path/ +# or ~user/. +#mail_full_filesystem_access = no + +# Dictionary for key=value mailbox attributes. This is used for example by +# URLAUTH and METADATA extensions. +#mail_attribute { +# dict file { +# path = %{home}/Maildir/dovecot-attributes +# } +#} + +# A comment or note that is associated with the server. This value is +# accessible for authenticated users through the IMAP METADATA server +# entry "/shared/comment". +#mail_server_comment = "" + +# Indicates a method for contacting the server administrator. According to +# RFC 5464, this value MUST be a URI (e.g., a mailto: or tel: URL), but that +# is currently not enforced. Use for example mailto:admin@example.com. This +# value is accessible for authenticated users through the IMAP METADATA server +# entry "/shared/admin". +#mail_server_admin = + +## +## Mail processes +## + +# Don't use mmap() at all. This is required if you store indexes to shared +# filesystems (NFS or clustered filesystem). +#mmap_disable = no + +# Rely on O_EXCL to work when creating dotlock files. NFS supports O_EXCL +# since version 3, so this should be safe to use nowadays by default. +#dotlock_use_excl = yes + +# When to use fsync() or fdatasync() calls: +# optimized (default): Whenever necessary to avoid losing important data +# always: Useful with e.g. NFS when write()s are delayed +# never: Never use it (best performance, but crashes can lose data) +#mail_fsync = optimized + +# Locking method for index files. Alternatives are fcntl, flock and dotlock. +# Dotlocking uses some tricks which may create more disk I/O than other locking +# methods. NFS users: flock doesn't work, remember to change mmap_disable. +#lock_method = fcntl + +# Directory where mails can be temporarily stored. Usually it's used only for +# mails larger than >= 128 kB. It's used by various parts of Dovecot, for +# example LDA/LMTP while delivering large mails or zlib plugin for keeping +# uncompressed mails. +#mail_temp_dir = /tmp + +# Valid UID range for users, defaults to 500 and above. This is mostly +# to make sure that users can't log in as daemons or other system users. +# Note that denying root logins is hardcoded to dovecot binary and can't +# be done even if first_valid_uid is set to 0. +#first_valid_uid = 500 +#last_valid_uid = 0 + +# Valid GID range for users, defaults to non-root/wheel. Users having +# non-valid GID as primary group ID aren't allowed to log in. If user +# belongs to supplementary groups with non-valid GIDs, those groups are +# not set. +#first_valid_gid = 1 +#last_valid_gid = 0 + +# Maximum allowed length for mail keyword name. It's only forced when trying +# to create new keywords. +#mail_max_keyword_length = 50 + +# ':' separated list of directories under which chrooting is allowed for mail +# processes (ie. /var/mail will allow chrooting to /var/mail/foo/bar too). +# This setting doesn't affect login_chroot, mail_chroot or auth chroot +# settings. If this setting is empty, "/./" in home dirs are ignored. +# WARNING: Never add directories here which local users can modify, that +# may lead to root exploit. Usually this should be done only if you don't +# allow shell access for users. +#valid_chroot_dirs = + +# Default chroot directory for mail processes. This can be overridden for +# specific users in user database by giving /./ in user's home directory +# (eg. /home/./user chroots into /home). Note that usually there is no real +# need to do chrooting, Dovecot doesn't allow users to access files outside +# their mail directory anyway. If your home directories are prefixed with +# the chroot directory, append "/." to mail_chroot. +#mail_chroot = + +# UNIX socket path to master authentication server to find users. +# This is used by imap (for shared users) and lda. +#auth_socket_path = /var/run/dovecot/auth-userdb + +# Directory where to look up mail plugins. +#mail_plugin_dir = /usr/lib/dovecot + +# Space separated list of plugins to load for all services. Plugins specific to +# IMAP, LDA, etc. are added to this list in their own .conf files. +#mail_plugins = +# +# To add plugins, use +#mail_plugins { +# plugin = yes +#} + +## +## Mailbox handling optimizations +## + +# Mailbox list indexes can be used to optimize IMAP STATUS commands. They are +# also required for IMAP NOTIFY extension to be enabled. +#mailbox_list_index = yes + +# Trust mailbox list index to be up-to-date. This reduces disk I/O at the cost +# of potentially returning out-of-date results after e.g. server crashes. +# The results will be automatically fixed once the folders are opened. +#mailbox_list_index_very_dirty_syncs = yes + +# Should INBOX be kept up-to-date in the mailbox list index? By default it's +# not, because most of the mailbox accesses will open INBOX anyway. +#mailbox_list_index_include_inbox = no + +# The minimum number of mails in a mailbox before updates are done to cache +# file. This allows optimizing Dovecot's behavior to do less disk writes at +# the cost of more disk reads. +#mail_cache_min_mail_count = 0 + +# When IDLE command is running, mailbox is checked once in a while to see if +# there are any new mails or other changes. This setting defines the minimum +# time to wait between those checks. Dovecot can also use inotify and +# kqueue to find out immediately when changes occur. +#mailbox_idle_check_interval = 30 secs + +# Save mails with CR+LF instead of plain LF. This makes sending those mails +# take less CPU, especially with sendfile() syscall with Linux and FreeBSD. +# But it also creates a bit more disk I/O which may just make it slower. +# Also note that if other software reads the mboxes/maildirs, they may handle +# the extra CRs wrong and cause problems. +#mail_save_crlf = no + +# Max number of mails to keep open and prefetch to memory. This only works with +# some mailbox formats and/or operating systems. +#mail_prefetch_count = 0 + +# How often to scan for stale temporary files and delete them (0 = never). +# These should exist only after Dovecot dies in the middle of saving mails. +#mail_temp_scan_interval = 1w + +# How many slow mail accesses sorting can perform before it returns failure. +# With IMAP the reply is: NO [LIMIT] Requested sort would have taken too long. +# The untagged SORT reply is still returned, but it's likely not correct. +#mail_sort_max_read_count = 0 + +protocol !indexer-worker { + # If folder vsize calculation requires opening more than this many mails from + # disk (i.e. mail sizes aren't in cache already), return failure and finish + # the calculation via indexer process. Disabled by default. This setting must + # be 0 for indexer-worker processes. + #mail_vsize_bg_after_count = 0 +} + +## +## Maildir-specific settings +## + +# By default LIST command returns all entries in maildir beginning with a dot. +# Enabling this option makes Dovecot return only entries which are directories. +# This is done by stat()ing each entry, so it causes more disk I/O. +# (For systems setting struct dirent->d_type, this check is free and it's +# done always regardless of this setting) +#maildir_stat_dirs = no + +# When copying a message, do it with hard links whenever possible. This makes +# the performance much better, and it's unlikely to have any side effects. +#maildir_copy_with_hardlinks = yes + +# Assume Dovecot is the only MUA accessing Maildir: Scan cur/ directory only +# when its mtime changes unexpectedly or when we can't find the mail otherwise. +#maildir_very_dirty_syncs = no + +# If enabled, Dovecot doesn't use the S= in the Maildir filenames for +# getting the mail's physical size, except when recalculating Maildir++ quota. +# This can be useful in systems where a lot of the Maildir filenames have a +# broken size. The performance hit for enabling this is very small. +#maildir_broken_filename_sizes = no + +# Always move mails from new/ directory to cur/, even when the \Recent flags +# aren't being reset. +#maildir_empty_new = no + +## +## mbox-specific settings +## + +# Which locking methods to use for locking mbox. There are four available: +# dotlock: Create .lock file. This is the oldest and most NFS-safe +# solution. If you want to use /var/mail/ like directory, the users +# will need write access to that directory. +# dotlock_try: Same as dotlock, but if it fails because of permissions or +# because there isn't enough disk space, just skip it. +# fcntl : Use this if possible. Works with NFS too if lockd is used. +# flock : May not exist in all systems. Doesn't work with NFS. +# lockf : May not exist in all systems. Doesn't work with NFS. +# +# You can use multiple locking methods; if you do the order they're declared +# in is important to avoid deadlocks if other MTAs/MUAs are using multiple +# locking methods as well. Some operating systems don't allow using some of +# them simultaneously. +#mbox_read_locks = fcntl +#mbox_write_locks = dotlock fcntl + +# Maximum time to wait for lock (all of them) before aborting. +#mbox_lock_timeout = 5 mins + +# If dotlock exists but the mailbox isn't modified in any way, override the +# lock file after this much time. +#mbox_dotlock_change_timeout = 2 mins + +# When mbox changes unexpectedly we have to fully read it to find out what +# changed. If the mbox is large this can take a long time. Since the change +# is usually just a newly appended mail, it'd be faster to simply read the +# new mails. If this setting is enabled, Dovecot does this but still safely +# fallbacks to re-reading the whole mbox file whenever something in mbox isn't +# how it's expected to be. The only real downside to this setting is that if +# some other MUA changes message flags, Dovecot doesn't notice it immediately. +# Note that a full sync is done with SELECT, EXAMINE, EXPUNGE and CHECK +# commands. +#mbox_dirty_syncs = yes + +# Like mbox_dirty_syncs, but don't do full syncs even with SELECT, EXAMINE, +# EXPUNGE or CHECK commands. If this is set, mbox_dirty_syncs is ignored. +#mbox_very_dirty_syncs = no + +# Delay writing mbox headers until doing a full write sync (EXPUNGE and CHECK +# commands and when closing the mailbox). This is especially useful for POP3 +# where clients often delete all mails. The downside is that our changes +# aren't immediately visible to other MUAs. +#mbox_lazy_writes = yes + +# If mbox size is smaller than this (e.g. 100k), don't write index files. +# If an index file already exists it's still read, just not updated. +#mbox_min_index_size = 0 + +# Mail header selection algorithm to use for MD5 POP3 UIDLs when +# pop3_uidl_format=%m. For backwards compatibility we use apop3d inspired +# algorithm, but it fails if the first Received: header isn't unique in all +# mails. An alternative algorithm is "all" that selects all headers. +#mbox_md5 = apop3d + +## +## mdbox-specific settings +## + +# Maximum dbox file size until it's rotated. +#mdbox_rotate_size = 10M + +# Maximum dbox file age until it's rotated. Typically in days. Day begins +# from midnight, so 1d = today, 2d = yesterday, etc. 0 = check disabled. +#mdbox_rotate_interval = 0 + +# When creating new mdbox files, immediately preallocate their size to +# mdbox_rotate_size. This setting currently works only in Linux with some +# filesystems (ext4, xfs). +#mdbox_preallocate_space = no + +# Settings to control adding $HasAttachment or $HasNoAttachment keywords. +# By default, all MIME parts with Content-Disposition=attachment, or inlines +# with filename parameter are consired attachments. +# add-flags - Add the keywords when saving new mails or when fetching can +# do it efficiently. +# content-type=type or !type - Include/exclude content type. Excluding will +# never consider the matched MIME part as attachment. Including will only +# negate an exclusion (e.g. content-type=!foo/* content-type=foo/bar). +# exclude-inlined - Exclude any Content-Disposition=inline MIME part. +#mail_attachment_detection_options = diff --git a/conf/conf.d/10-master.conf b/conf/conf.d/10-master.conf new file mode 100644 index 0000000..fc5b288 --- /dev/null +++ b/conf/conf.d/10-master.conf @@ -0,0 +1,133 @@ +#default_process_limit = 100 +#default_client_limit = 1000 + +# Default VSZ (virtual memory size) limit for service processes. This is mainly +# intended to catch and kill processes that leak memory before they eat up +# everything. +#default_vsz_limit = 256M + +# Login user is internally used by login processes. This is the most untrusted +# user in Dovecot system. It shouldn't have access to anything at all. +#default_login_user = dovenull + +# Internal user is used by unprivileged processes. It should be separate from +# login user, so that login processes can't disturb other processes. +#default_internal_user = dovecot + +service imap-login { + inet_listener imap { + #port = 143 + } + inet_listener imaps { + #port = 993 + #ssl = yes + } + + # Number of connections to handle before starting a new process. Typically + # the only useful values are 0 (unlimited) or 1. 1 is more secure, but 0 + # is faster. + #service_restart_request_count = 1 + + # Number of processes to always keep waiting for more connections. + #process_min_avail = 0 + + # If you set service_restart_request_count=0, you probably need to grow this. + #vsz_limit = 256M # default +} + +service pop3-login { + inet_listener pop3 { + #port = 110 + } + inet_listener pop3s { + #port = 995 + #ssl = yes + } +} + +service submission-login { + inet_listener submission { + #port = 587 + } + inet_listener submissions { + #port = 465 + } +} + +service lmtp { + unix_listener lmtp { + #mode = 0666 + } + + # Create inet listener only if you can't use the above UNIX socket + #inet_listener lmtp { + # Avoid making LMTP visible for the entire internet + #listen = 127.0.0.1 + #port = 24 + #} +} + +service imap { + # Most of the memory goes to mmap()ing files. You may need to increase this + # limit if you have huge mailboxes. + #vsz_limit = 256M # default + + # Max. number of IMAP processes (connections) + #process_limit = 1024 +} + +service pop3 { + # Max. number of POP3 processes (connections) + #process_limit = 1024 +} + +service submission { + # Max. number of SMTP Submission processes (connections) + #process_limit = 1024 +} + +service auth { + # auth_socket_path points to this userdb socket by default. It's typically + # used by dovecot-lda, doveadm, possibly imap process, etc. Users that have + # full permissions to this socket are able to get a list of all usernames and + # get the results of everyone's userdb lookups. + # + # The default 0666 mode allows anyone to connect to the socket, but the + # userdb lookups will succeed only if the userdb returns an "uid" field that + # matches the caller process's UID. Also if caller's uid or gid matches the + # socket's uid or gid the lookup succeeds. Anything else causes a failure. + # + # To give the caller full permissions to lookup all users, set the mode to + # something else than 0666 and Dovecot lets the kernel enforce the + # permissions (e.g. 0777 allows everyone full permissions). + unix_listener auth-userdb { + #mode = 0666 + #user = + #group = + } + + # Postfix smtp-auth + #unix_listener /var/spool/postfix/private/auth { + # mode = 0666 + #} + + # Auth process is run as this user. + #user = $SET:default_internal_user +} + +service auth-worker { + # Auth worker process is run as root by default, so that it can access + # /etc/shadow. If this isn't necessary, the user should be changed to + # $SET:default_internal_user. + #user = root +} + +service dict { + # If dict proxy is used, mail processes should have access to its socket. + # For example: mode=0660, group=vmail and global mail_access_groups=vmail + unix_listener dict { + #mode = 0600 + #user = + #group = + } +} diff --git a/conf/conf.d/10-metrics.conf b/conf/conf.d/10-metrics.conf new file mode 100644 index 0000000..36d1729 --- /dev/null +++ b/conf/conf.d/10-metrics.conf @@ -0,0 +1,122 @@ +## +## Statistics and metrics +## + +# Dovecot supports gathering statistics from events. +# Currently there are no statistics logged by default, and therefore they must +# be explicitly added using the metric configuration blocks. +# +# Unlike old stats, the new statistics do not require any plugins loaded. +# +# See https://doc.dovecot.org/latest/core/config/statistics.html for details + +## +## Example metrics +## + +#metric auth_success { +# filter = (event=auth_request_finished AND success=yes) +#} +# +#metric auth_failure { +# filter = (event=auth_request_finished AND NOT success=yes) +## See exporter config later in this file +## can be used to replace auth_verbose=yes +# exporter = log +#} +# +#metric imap_command { +# filter = event=imap_command_finished +# group_by cmd_name { +# method discrete { +# } +# } +# group_by tagged_reply_state { +# method discrete { +# } +# } +#} +# +#metric smtp_command { +# filter = event=smtp_server_command_finished and protocol=submission +# group_by cmd_name { +# method discrete { +# } +# } +# group_by status_code { +# method discrete { +# } +# } +# group_by duration { +# method exponential { +# base = 10 +# min_magnitude = 1 +# max_magnitude = 5 +# } +# } +#} +# +#metric lmtp_command { +# filter = event=smtp_server_command_finished and protocol=lmtp +# group_by cmd_name { +# method discrete { +# } +# } +# group_by status_code { +# method discrete { +# } +# } +# group_by duration { +# method exponential { +# base = 10 +# min_magnitude = 1 +# max_magnitude = 5 +# } +# } +#} +# +#metric mail_delivery { +# filter = event=mail_delivery_finished +# group_by duration { +# method exponential { +# base = 10 +# min_magnitude = 1 +# max_magnitude = 5 +# } +# } +#} + +## +## Prometheus +## + +# To allow access to statistics with Prometheus, enable http listener +# on stats process. Stats will be available on /metrics path. +# +# See https://doc.dovecot.org/latest/core/config/statistics.html#openmetrics for more +# details. + +#service stats { +# inet_listener http { +# port = 9900 +# } +#} + +## +## Event exporting +## + +# You can also export individual events. +# +# See https://doc.dovecot.org/configuration_manual/event_export/ for more +# details. + +#event_exporter log { +# format = json +# time_format = rfc3339 +#} +# +#metric imap_commands { +# exporter = log +# filter = event=imap_command_finished +#} diff --git a/conf/conf.d/10-ssl.conf b/conf/conf.d/10-ssl.conf new file mode 100644 index 0000000..d48b15c --- /dev/null +++ b/conf/conf.d/10-ssl.conf @@ -0,0 +1,56 @@ +## +## SSL settings +## + +# SSL/TLS support: yes, no, required. +ssl = yes + +# PEM encoded X.509 SSL/TLS certificate and private key. By default, Debian +# installs a self-signed certificate. This is useful for testing, but you +# should obtain a real certificate from a recognized certificate authority. +# +# These files are opened before dropping root privileges, so keep the key file +# unreadable by anyone but root. Included /usr/share/dovecot/mkcert.sh can be +# used to easily generate self-signed certificate, just make sure to update the +# domains in dovecot-openssl.cnf +# +# Preferred permissions: root:root 0444 +ssl_server_cert_file = /etc/dovecot/private/dovecot.pem +# Preferred permissions: root:root 0400 +ssl_server_key_file = /etc/dovecot/private/dovecot.key + +# If key file is password protected, give the password here. Alternatively +# give it when starting dovecot with -p parameter. Since this file is often +# world-readable, you may want to place this setting instead to a different +# root owned 0600 file by using ssl_key_password = was automatically rejected:%n%r + +# Delimiter character between local-part and detail in email address. +#recipient_delimiter = + + +# Header where the original recipient address (SMTP's RCPT TO: address) is taken +# from if not available elsewhere. With dovecot-lda -a parameter overrides this. +# A commonly used header for this is X-Original-To. +#lda_original_recipient_header = + +# Should saving a mail to a nonexistent mailbox automatically create it? +#lda_mailbox_autocreate = no + +# Should automatically created mailboxes be also automatically subscribed? +#lda_mailbox_autosubscribe = no + +protocol lda { + # Boolean list of plugins to load + #mail_plugins { + # sieve = yes + #} +} diff --git a/conf/conf.d/15-mailboxes.conf b/conf/conf.d/15-mailboxes.conf new file mode 100644 index 0000000..71076d4 --- /dev/null +++ b/conf/conf.d/15-mailboxes.conf @@ -0,0 +1,86 @@ +## +## Mailbox definitions +## + +# Each mailbox is specified in a separate mailbox section. The section name +# specifies the mailbox name. If it has spaces, you can put the name +# "in quotes". These sections can contain the following mailbox settings: +# +# auto: +# Indicates whether the mailbox with this name is automatically created +# implicitly when it is first accessed. The user can also be automatically +# subscribed to the mailbox after creation. The following values are +# defined for this setting: +# +# no - Never created automatically. +# create - Automatically created, but no automatic subscription. +# subscribe - Automatically created and subscribed. +# +# special_use: +# A space-separated list of SPECIAL-USE flags (RFC 6154) to use for the +# mailbox. There are no validity checks, so you could specify anything +# you want in here, but it's not a good idea to use flags other than the +# standard ones specified in the RFC: +# +# \All - This (virtual) mailbox presents all messages in the +# user's message store. +# \Archive - This mailbox is used to archive messages. +# \Drafts - This mailbox is used to hold draft messages. +# \Flagged - This (virtual) mailbox presents all messages in the +# user's message store marked with the IMAP \Flagged flag. +# \Important - This (virtual) mailbox presents all messages in the +# user's message store deemed important to user. +# \Junk - This mailbox is where messages deemed to be junk mail +# are held. +# \Sent - This mailbox is used to hold copies of messages that +# have been sent. +# \Trash - This mailbox is used to hold messages that have been +# deleted. +# +# comment: +# Defines a default comment or note associated with the mailbox. This +# value is accessible through the IMAP METADATA mailbox entries +# "/shared/comment" and "/private/comment". Users with sufficient +# privileges can override the default value for entries with a custom +# value. + +# NOTE: Assumes "namespace inbox" has been defined in 10-mail.conf. +namespace inbox { + # These mailboxes are widely used and could perhaps be created automatically: + mailbox Drafts { + special_use = \Drafts + } + mailbox Junk { + special_use = \Junk + } + mailbox Trash { + special_use = \Trash + } + + # For \Sent mailboxes there are two widely used names. We'll mark both of + # them as \Sent. User typically deletes one of them if duplicates are created. + mailbox Sent { + special_use = \Sent + } + mailbox "Sent Messages" { + special_use = \Sent + } + + # If you have a virtual "All messages" mailbox: + #mailbox virtual/All { + # special_use = \All + # comment = All my messages + #} + + # If you have a virtual "Flagged" mailbox: + #mailbox virtual/Flagged { + # special_use = \Flagged + # comment = All my flagged messages + #} + + # If you have a virtual "Important" mailbox: + #mailbox virtual/Important { + # special_use = \Important + # comment = All my important messages + #} +} diff --git a/conf/conf.d/20-imap.conf b/conf/conf.d/20-imap.conf new file mode 100644 index 0000000..34610e9 --- /dev/null +++ b/conf/conf.d/20-imap.conf @@ -0,0 +1,108 @@ +## +## IMAP specific settings +## + +# If nothing happens for this long while client is IDLEing, move the connection +# to imap-hibernate process and close the old imap process. This saves memory, +# because connections use very little memory in imap-hibernate process. The +# downside is that recreating the imap process back uses some resources. +#imap_hibernate_timeout = 0 + +# Maximum IMAP command line length. Some clients generate very long command +# lines with huge mailboxes, so you may need to raise this if you get +# "Too long argument" or "IMAP command line too large" errors often. +#imap_max_line_length = 64k + +# IMAP logout format string: +# %{input} - total number of bytes read from client +# %{output} - total number of bytes sent to client +# %{fetch_hdr_count} - Number of mails with mail header data sent to client +# %{fetch_hdr_bytes} - Number of bytes with mail header data sent to client +# %{fetch_body_count} - Number of mails with mail body data sent to client +# %{fetch_body_bytes} - Number of bytes with mail body data sent to client +# %{deleted} - Number of mails where client added \Deleted flag +# %{expunged} - Number of mails that client expunged, which does not +# include automatically expunged mails +# %{autoexpunged} - Number of mails that were automatically expunged after +# client disconnected +# %{trashed} - Number of mails that client copied/moved to the +# special_use=\Trash mailbox. +# %{appended} - Number of mails saved during the session +#imap_logout_format = in=%i out=%o deleted=%{deleted} expunged=%{expunged} \ +# trashed=%{trashed} hdr_count=%{fetch_hdr_count} \ +# hdr_bytes=%{fetch_hdr_bytes} body_count=%{fetch_body_count} \ +# body_bytes=%{fetch_body_bytes} + +# Amend or override the IMAP capability response. To override, set the value +# with imap_capability = +# +# To amend, you can use a boolean list to specify which capabilities to turn +# on and off +#imap_capability { +# SPECIAL-USE = yes +# "LITERAL+" = no +#} + +# How long to wait between "OK Still here" notifications when client is +# IDLEing. +#imap_idle_notify_interval = 2 mins + +# ID field names and values to send to clients. Using * as the value makes +# Dovecot use the default value. The following fields have default values +# currently: name, version, os, os-version, support-url, support-email, +# revision. +#imap_id_send = + +# Use imap_id_received event to log IMAP id + +# Workarounds for various client bugs: +# delay-newmail: +# Send EXISTS/RECENT new mail notifications only when replying to NOOP +# and CHECK commands. Some clients ignore them otherwise, for example OSX +# Mail () instead of full path +# syntax. +# +#lmtp_client_workarounds { +# whitespace-before-path = yes +#} + +protocol lmtp { + #mail_plugins { + # sieve = yes + #} + + # This strips the domain name before delivery, since the default + # userdb in Debian is /etc/passwd, which doesn't include domain + # names in the user. If you're using a different userdb backend + # that does include domain names, you may wish to remove this. See + # https://doc.dovecot.org/2.4.1/howto/lmtp/exim.html and + # https://doc.dovecot.org/2.4.1/core/summaries/settings.html#auth_username_format + auth_username_format = %{user | username | lower} +} diff --git a/conf/conf.d/20-managesieve.conf b/conf/conf.d/20-managesieve.conf new file mode 100644 index 0000000..bcee3b8 --- /dev/null +++ b/conf/conf.d/20-managesieve.conf @@ -0,0 +1,78 @@ +## +## ManageSieve specific settings +## + +# Uncomment to enable managesieve protocol: +protocols { + sieve = yes +} + +# Service definitions + +service managesieve-login { + inet_listener sieve { + port = 4190 + } + + inet_listener sieve_deprecated { + port = 2000 + } + + # Number of connections to handle before starting a new process. Typically + # the only useful values are 0 (unlimited) or 1. 1 is more secure, but 0 + # is faster. + #service_restart_request_count = 1 + + # Number of processes to always keep waiting for more connections. + #process_min_avail = 0 + + # If you set service_restart_request_count=0, you probably need to grow this. + #vsz_limit = 64M +} + +service managesieve { + # Max. number of ManageSieve processes (connections) + #process_limit = 1024 +} + +# Service configuration + +protocol sieve { + # Maximum ManageSieve command line length in bytes. ManageSieve usually does + # not involve overly long command lines, so this setting will not normally + # need adjustment + #managesieve_max_line_length = 65536 + + # Maximum number of ManageSieve connections allowed for a user from each IP + # address. + # NOTE: The username is compared case-sensitively. + #mail_max_userip_connections = 10 + + # Space separated list of plugins to load (none known to be useful so far). + # Do NOT try to load IMAP plugins here. + #mail_plugins = + + # MANAGESIEVE logout format string: + #managesieve_logout_format = bytes=%{input}/%{output} + + # To fool ManageSieve clients that are focused on CMU's timesieved you can + # specify the IMPLEMENTATION capability that Dovecot reports to clients. + # For example: 'Cyrus timsieved v2.2.13' + #managesieve_implementation_string = 'Cyrus timsieved v2.2.13' + + # Explicitly specify the SIEVE and NOTIFY capability reported by the server + # before login. If left unassigned these will be reported dynamically + # according to what the Sieve interpreter supports by default (after login + # this may differ depending on the user). + #managesieve_sieve_capability { + #} + #managesieve_notify_capability { + #} + + # The maximum number of compile errors that are returned to the client upon + # script upload or script verification. + #managesieve_max_compile_errors = 5 + + # Refer to 90-sieve.conf for script quota configuration and configuration of + # Sieve execution limits. +} diff --git a/conf/conf.d/20-pop3.conf b/conf/conf.d/20-pop3.conf new file mode 100644 index 0000000..f034dbc --- /dev/null +++ b/conf/conf.d/20-pop3.conf @@ -0,0 +1,101 @@ +## +## POP3 specific settings +## + +# Don't try to set mails non-recent or seen with POP3 sessions. This is +# mostly intended to reduce disk I/O. With maildir it doesn't move files +# from new/ to cur/, with mbox it doesn't write Status-header. +#pop3_no_flag_updates = no + +# Support LAST command which exists in old POP3 specs, but has been removed +# from new ones. Some clients still wish to use this though. Enabling this +# makes RSET command clear all \Seen flags from messages. +#pop3_enable_last = no + +# If mail has X-UIDL header, use it as the mail's UIDL. +#pop3_reuse_xuidl = no + +# Allow only one POP3 session to run simultaneously for the same user. +#pop3_lock_session = no + +# POP3 requires message sizes to be listed as if they had CR+LF linefeeds. +# Many POP3 servers violate this by returning the sizes with LF linefeeds, +# because it's faster to get. When this setting is enabled, Dovecot still +# tries to do the right thing first, but if that requires opening the +# message, it fallbacks to the easier (but incorrect) size. +#pop3_fast_size_lookups = no + +# POP3 UIDL (unique mail identifier) format to use. You can use following +# variables with variable extension as described in +# https://doc.dovecot.org/latest/core/settings/variables.html +# +# %{uidvalidity} - Mailbox's IMAP UIDVALIDITY +# %{uid} - Mail's IMAP UID +# %{md5} - MD5 sum of the mailbox headers in hex (mbox only) +# %{filename} - filename (maildir only) +# %{guid} - Mail's GUID +# +# If you want UIDL compatibility with other POP3 servers, use: +# UW's ipop3d : %{uid | hex(8)}%{uidvalidity | hex(8)} +# Courier : %{filename} or %{uidvalidity}-%{uid} (both might be used simultaneously) +# Cyrus (<= 2.1.3) : %{uid} +# Cyrus (>= 2.1.4) : %{uidvalidity}.%{uid} +# Dovecot v0.99.x : %{uidvalidity}.%{uid} +# tpop3d : %{filename|md5} +# +# Note that Outlook 2003 seems to have problems with %{uidvalidity}.%{uid} format which was +# Dovecot's default, so if you're building a new server it would be a good +# idea to change this. %{uid | hex(8)}%{uidvalidity | hex(8)} should be pretty fail-safe. +# +#pop3_uidl_format = %{uid | hex(8)}%{uidvalidity | hex(8)} + +# Permanently save UIDLs sent to POP3 clients, so pop3_uidl_format changes +# won't change those UIDLs. Currently this works only with Maildir. +#pop3_save_uidl = no + +# What to do about duplicate UIDLs if they exist? +# allow: Show duplicates to clients. +# rename: Append a temporary -2, -3, etc. counter after the UIDL. +#pop3_uidl_duplicates = allow + +# This option changes POP3 behavior so that it's not possible to actually +# delete mails via POP3, only hide them from future POP3 sessions. The mails +# will still be counted towards user's quota until actually deleted via IMAP. +# Use e.g. "$POP3Deleted" as the value (it will be visible as IMAP keyword). +# Make sure you can legally archive mails before enabling this setting. +#pop3_deleted_flag = + +# POP3 logout format string: +# %{input} - Bytes read from the client +# %{output} - Bytes sent to the client +# %{top_count} - Number of TOP commands run +# %{top_bytes} - Bytes sent to the client because of TOP commands +# %{retr_count} - Number of RETR commands run +# %{retr_bytes} - Bytes sent to the client because of RETR commands +# %{deleted_count} - Number of deleted messages +# %{deleted_bytes} - Number of bytes in deleted messages +# %{message_count} - Number of messages before deletion +# %{message_bytes} - Mailbox size, in bytes, before deletion +# %{uidl_change} - The old and the new UIDL hash (which can be useful for identifying unexpected changes in UIDLs) +#pop3_logout_format = top=%{top_count}/%{top_bytes}, retr=%{retr_count}/%{retr_bytes}, del=%{deleted_count}/%{deleted_bytes}, size=%{message_bytes} + +# Workarounds for various client bugs: +# outlook-no-nuls: +# Outlook and Outlook Express hang if mails contain NUL characters. +# This setting replaces them with 0x80 character. +# oe-ns-eoh: +# Outlook Express and Netscape Mail breaks if end of headers-line is +# missing. This option simply sends it if it's missing. +#pop3_client_workarounds { +# outlook-no-nuls = yes +#} + +protocol pop3 { + # Space separated list of plugins to load (default is global mail_plugins). + #mail_plugins { + #} + + # Maximum number of POP3 connections allowed for a user from each IP address. + # NOTE: The username is compared case-sensitively. + #mail_max_userip_connections = 10 +} diff --git a/conf/conf.d/20-submission.conf b/conf/conf.d/20-submission.conf new file mode 100644 index 0000000..1064bca --- /dev/null +++ b/conf/conf.d/20-submission.conf @@ -0,0 +1,114 @@ +## +## Settings specific to SMTP Submission +## + +# SMTP Submission logout format string: +# %{input} - total number of bytes read from client +# %{output}- total number of bytes sent to client +# %{command_count} - Number of commands received from client +# %{reply_count} - Number of replies sent to client +# %{session} - Session ID of the login session +# %{transaction_id} - ID of the current transaction, if any +#submission_logout_format = in=%{input} out=%{output} + +# Host name reported by the SMTP service, for example to the client in the +# initial greeting and to the relay server in the HELO/EHLO command. +# Default is the system's real hostname@domain. +#hostname = + +# Maximum size of messages accepted for relay. This announced in the SIZE +# capability. If not configured, this is either determined from the relay +# server or left unlimited if no limit is known (relay will reply with error +# if some unknown limit exists there, which is duly passed to our client). +#submission_max_mail_size = + +# Maximum number of recipients accepted per connection (default: unlimited) +#submission_max_recipients = + +# Workarounds for various client bugs: +# whitespace-before-path: +# Allow one or more spaces or tabs between `MAIL FROM:' and path and between +# `RCPT TO:' and path. +# mailbox-for-path: +# Allow using bare Mailbox syntax (i.e., without <...>) instead of full path +# syntax. +# +#submission_client_workarounds { +# whitespace-before-path = yes +#} + +# Relay server configuration: +# +# The Dovecot SMTP submission service directly proxies the mail transaction +# to the SMTP relay configured here. + +# Host name for the relay server (required) +#submission_relay_host = + +# Port for the relay server +#submission_relay_port = 25 + +# Is the relay server trusted? This determines whether we try to send +# (Postfix-specific) XCLIENT data to the relay server +#submission_relay_trusted = no + +# Authentication data for the relay server if authentication is required +#submission_relay_user = +#submission_relay_master_user = +#submission_relay_password = + +# SSL configuration for connection to relay server +# +# submission_relay_ssl: +# Indicates whether SSL is used for the connection to the relay server. The +# following values are defined for this setting: +# +# no - No SSL is used +# smtps - An SMTPS connection (immediate SSL) is used +# starttls - The STARTTLS command is used to establish SSL layer +#submission_relay_ssl = no + +# submission_relay_ssl_verify: +# Configures whether the SSL certificate of the relay server is to be +# verified. +#submission_relay_ssl_verify = yes + +# Write protocol logs for relay connection to this directory for debugging +#submission_relay_rawlog_dir = + +# BURL is configured implicitly by IMAP URLAUTH + +# Part of the SMTP capabilities that the submission service can offer to the +# client (as listed in the EHLO reply) depend on those capabilities also being +# provided by the relay server. These capabilities currently are: +# +# - 8BITMIME +# - BINARYMIME +# - DSN +# - VRFY (always returns 252 without support) +# +# By default, the submission service first connects to the relay server to +# determine the support for such capabilities before sending the initial EHLO +# reply to the client. If the list of capabilities returned by the relay server +# is somehow unreliable or it is undesirable to start the connection to the +# relay server before the first mail transaction is started, the backend +# capabilities can be configured explicitly using the +# submission_backend_capabilities setting. This is a space-separated list of +# SMTP capability names. This setting is only relevant for capabilities that +# depend on support from the relay server: including (or omitting) capabilities +# that are not listed above has no effect. When this setting is explicitly set +# to the empty string, none of the capabilities is enabled. To achieve the +# default behavior, this setting must be left unconfigured. +#submission_backend_capabilities = + +protocol submission { + # Space-separated list of plugins to load (default is global mail_plugins). + #mail_plugins { + #} + + # Maximum number of SMTP submission connections allowed for a user from + # each IP address. + # NOTE: The username is compared case-sensitively. + #mail_max_userip_connections = 10 +} + diff --git a/conf/conf.d/30-dict-server.conf b/conf/conf.d/30-dict-server.conf new file mode 100644 index 0000000..5ff34b0 --- /dev/null +++ b/conf/conf.d/30-dict-server.conf @@ -0,0 +1,34 @@ +## +## Dictionary server settings +## + +# Dictionary can be used to store key=value lists. This is used by several +# plugins. The dictionary can be accessed either directly or though a +# dictionary server. The following dict block maps dictionary names to URIs +# when the server is used. These can then be referenced using URIs in format +# "proxy::". + +dict_server { +# dict quota { +# driver = sql +# sql_driver = mysql +# hostname = localhost +# +# dict_map priv/quota/storage { +# sql_table = quota +# username_field = username +# value_field bytes { +# type = uint +# } +# } +# +# dict_map priv/quota/messages { +# sql_table = quota +# username_field = username +# value_field messages { +# type = uint +# } +# } +# } +} + diff --git a/conf/conf.d/90-acl.conf b/conf/conf.d/90-acl.conf new file mode 100644 index 0000000..b3fcfb4 --- /dev/null +++ b/conf/conf.d/90-acl.conf @@ -0,0 +1,30 @@ +## +## Mailbox access control lists. +## + +# vfile backend reads ACLs from "dovecot-acl" file from mail directory. +# You can also optionally give a global ACL directory path where ACLs are +# applied to all users' mailboxes. The global ACL directory contains +# one file for each mailbox, eg. INBOX or sub.mailbox. cache_secs parameter +# specifies how many seconds to wait between stat()ing dovecot-acl file +# to see if it changed. + +#acl_driver = vfile +## Deprecated ACL global path +#acl_global_path = /etc/dovecot/global-acls +#acl_cache_ttl = 5m + +## New inline ACLs +#mailbox INBOX { +# acl user=testuser { +# rights = lri +# } +#} + +# To let users LIST mailboxes shared by other users, Dovecot needs a +# shared mailbox dictionary. For example: +#acl_sharing_map { +# dict file { +# path = /var/lib/dovecot/shared-mailboxes +# } +#} diff --git a/conf/conf.d/90-fts-flatcurve.conf b/conf/conf.d/90-fts-flatcurve.conf new file mode 100644 index 0000000..da7b1de --- /dev/null +++ b/conf/conf.d/90-fts-flatcurve.conf @@ -0,0 +1,29 @@ +mail_plugins { + fts = yes + fts_flatcurve = yes +} + +fts_autoindex = yes + +language_filters = normalizer-icu snowball stopwords + +language_tokenizers = generic email-address +language_tokenizer_generic_algorithm = simple + +language en { + default = yes + filters = lowercase snowball english-possessive stopwords +} + +fts flatcurve { + # All of these are optional, and indicate the default values. + # They are listed here for documentation purposes; most people should not + # need to define/override in their config. + # commit_limit = 500 + # max_term_size = 30 + # min_term_size = 2 + # optimize_limit = 10 + # rotate_count = 5000 + # rotate_time = 5000 + substring_search = yes +} diff --git a/conf/conf.d/90-fts.conf b/conf/conf.d/90-fts.conf new file mode 100644 index 0000000..e095c9b --- /dev/null +++ b/conf/conf.d/90-fts.conf @@ -0,0 +1,26 @@ +## +## Plugin settings +## + +# All wanted plugins must be listed in mail_plugins setting before any of the +# settings take effect. See for list of plugins and +# their configuration. Note that %{variable} expansion is done for all values. + +## See https://doc.dovecot.org/latest/core/plugins/fts.html for FTS information + +fts_autoindex = yes +fts_autoindex_max_recent_msgs = 999 +fts_search_add_missing = yes + +## for attachment decoding + +#fts_decoder_driver = tika +#fts_decoder_tika_url = http://localhost:9998/tika/ + +## for solr +#fts solr { +# url = http://localhost:8983/solr/dovecot/ +#} + +# See 90-fts-flatcurve.conf in the dovecot-flatcurve package for +# settings specific to that driver diff --git a/conf/conf.d/90-quota.conf b/conf/conf.d/90-quota.conf new file mode 100644 index 0000000..f3eb2a1 --- /dev/null +++ b/conf/conf.d/90-quota.conf @@ -0,0 +1,79 @@ +## +## Quota configuration. +## + +# Note that you also have to enable quota plugin in mail_plugins setting. +## + +## +## Quota limits +## + +# Quota limits are set using "quota_rule" parameters. To get per-user quota +# limits, you can set/override them by returning "quota_rule" extra field +# from userdb. It's also possible to give mailbox-specific limits, for example +# to give additional 100 MB when saving to Trash: + +#mail_plugins { +# quota = yes +#} + +#quota "User quota" { +# storage_size = 1G +#} +# +#namespace inbox { +# mailbox Trash { +# quota_storage_extra = 100M +# } +#} + +## +## Quota warnings +## + +# You can execute a given command when user exceeds a specified quota limit. +# Each quota root has separate limits. Only the command for the first +# exceeded limit is executed, so put the highest limit first. +# The commands are executed via script service by connecting to the named +# UNIX socket (quota-warning below). +# Note that % needs to be escaped as %%, otherwise "% " expands to empty. + +#quota "User quota" { +# warning warn-95 { +# quota_storage_percentage = 95 +# execute quota-warning { +# args = 95 %{user} +# } +# } +# warning warn-80 { +# quota_storage_percentage = 80 +# execute quota-warning { +# args = 80 %{user} +# } +# } +#} + +# Example quota-warning service. The unix listener's permissions should be +# set in a way that mail processes can connect to it. Below example assumes +# that mail processes run as vmail user. If you use mode=0666, all system users +# can generate quota warnings to anyone. +#service quota-warning { +# executable = script /usr/local/bin/quota-warning.sh +# user = dovecot +# unix_listener quota-warning { +# user = vmail +# } +#} + +## +## Quota backends +## + +# Multiple backends are supported: +# count: Default and recommended, quota driver tracks the quota internally within Dovecot's index files. +# maildir: Maildir++ quota +# fs: Read-only support for filesystem quota +#quota "User quota" { +# driver = count +#} diff --git a/conf/conf.d/90-sieve-extprograms.conf b/conf/conf.d/90-sieve-extprograms.conf new file mode 100644 index 0000000..4998fc9 --- /dev/null +++ b/conf/conf.d/90-sieve-extprograms.conf @@ -0,0 +1,41 @@ +# Sieve Extprograms plugin configuration + +# Don't forget to add the sieve_extprograms plugin to the sieve_plugins setting. +# Also enable the extensions you need (one or more of vnd.dovecot.pipe, +# vnd.dovecot.filter and vnd.dovecot.execute) by adding these to the +# sieve_extensions or sieve_global_extensions settings. Restricting these +# extensions to a global context using sieve_global_extensions is recommended. + +# The directory where the program sockets are located for the +# vnd.dovecot.pipe, vnd.dovecot.filter and vnd.dovecot.execute extension +# respectively. The name of each unix socket contained in that directory +# directly maps to a program-name referenced from the Sieve script. +#sieve_pipe_socket_dir = sieve-pipe +#sieve_filter_socket_dir = sieve-filter +#sieve_execute_socket_dir = sieve-execute + +# The directory where the scripts are located for direct execution by the +# vnd.dovecot.pipe, vnd.dovecot.filter and vnd.dovecot.execute extension +# respectively. The name of each script contained in that directory +# directly maps to a program-name referenced from the Sieve script. +#sieve_pipe_bin_dir = /usr/lib/dovecot/sieve-pipe +#sieve_filter_bin_dir = /usr/lib/dovecot/sieve-filter +#sieve_execute_bin_dir = /usr/lib/dovecot/sieve-execute + +# An example program service called 'do-something' to pipe messages to +#service do-something { + # Define the executed script as parameter to the sieve service + #executable = script /usr/lib/dovecot/sieve-pipe/do-something.sh + + # Use some unprivileged user for executing the program + #user = dovenull + + # The unix socket located in the sieve_pipe_socket_dir (as defined in the + # plugin {} section above) + #unix_listener sieve-pipe/do-something { + # LDA/LMTP must have access + # user = vmail + # mode = 0600 + #} +#} + diff --git a/conf/conf.d/90-sieve.conf b/conf/conf.d/90-sieve.conf new file mode 100644 index 0000000..3ab66d3 --- /dev/null +++ b/conf/conf.d/90-sieve.conf @@ -0,0 +1,113 @@ +## +## Settings for the Sieve interpreter +## + +# Do not forget to enable the Sieve plugin in 15-lda.conf and 20-lmtp.conf +# by adding it to the respective mail_plugins { sieve = yes } settings. + +# See https://doc.dovecot.org/latest/core/plugins/sieve.html + +# Personal sieve script location +#sieve_script personal { +# driver = file +# path = ~/sieve +# active_path = ~/.dovecot.sieve +#} + +# Default sieve script location +#sieve_script default { +# type = default +# name = default +# driver = file +# path = /etc/dovecot/sieve/default/ +#} + + +# Which Sieve language extensions are available to users. By default, all +# supported extensions are available, except for deprecated extensions or +# those that are still under development. Some system administrators may want +# to disable certain Sieve extensions or enable those that are not available +# by default. This setting can use 'yes' and 'no' to specify differences relative +# to the default. For example `imapflags = yes' will enable the +# deprecated imapflags extension in addition to all extensions were already +# enabled by default. +#sieve_extensions { +# mboxmetadata = yes +# vnd.dovecot.debug = yes +#} + +# Which Sieve language extensions are ONLY available in global scripts. This +# can be used to restrict the use of certain Sieve extensions to administrator +# control, for instance when these extensions can cause security concerns. +# This setting has higher precedence than the `sieve_extensions' setting +# (above), meaning that the extensions enabled with this setting are never +# available to the user's personal script no matter what is specified for the +# `sieve_extensions' setting. The syntax of this setting is similar to the +# `sieve_extensions' setting, with the difference that extensions are +# enabled or disabled for exclusive use in global scripts. Currently, no +# extensions are marked as such by default. +#sieve_global_extensions = + +# The Pigeonhole Sieve interpreter can have plugins of its own. Using this +# setting, the used plugins can be specified. Check the Dovecot documentation +# https://doc.dovecot.org/latest/core/plugins/sieve.html + +#sieve_plugins = sieve_imapsieve sieve_extprograms +#sieve_pipe_bin_dir = /usr/share/dovecot-pigeonhole/sieve +#sieve_execute_bin_dir = /usr/share/dovecot-pigeonhole/sieve +#sieve_global_extensions { +# vnd.dovecot.pipe = yes +# vnd.dovecot.execute = yes +#} +#imapsieve_url = + +# The separator that is expected between the :user and :detail +# address parts introduced by the subaddress extension. This may +# also be a sequence of characters (e.g. '--'). The current +# implementation looks for the separator from the left of the +# localpart and uses the first one encountered. The :user part is +# left of the separator and the :detail part is right. This setting +# is also used by Dovecot's LMTP service. +#recipient_delimiter = +-_ + +# The maximum size of a Sieve script. The compiler will refuse to compile any +# script larger than this limit. If set to 0, no limit on the script size is +# enforced. +#sieve_max_script_size = 1M + +# The maximum number of actions that can be performed during a single script +# execution. If set to 0, no limit on the total number of actions is enforced. +#sieve_max_actions = 32 + +# The maximum number of redirect actions that can be performed during a single +# script execution. If set to 0, no redirect actions are allowed. +#sieve_max_redirects = 4 + +# The maximum number of personal Sieve scripts a single user can have. If set +# to 0, no limit on the number of scripts is enforced. +# (Currently only relevant for ManageSieve) +#sieve_quota_script_count = 0 + +# The maximum amount of disk storage a single user's scripts may occupy. If +# set to 0, no limit on the used amount of disk storage is enforced. +# (Currently only relevant for ManageSieve) +#sieve_quota_storage_size = 0 + + +#mailbox Spam { +## From elsewhere to Spam folder +# sieve_script report-spam { +# type = before +# cause = copy +# path = /etc/dovecot/report-spam.sieve +# } +#} + +## From Spam folder to elsewhere +#imapsieve_from Spam { +# sieve_script report-ham { +# type = before +# cause = copy +# path = /etc/dovecot/report-ham.sieve +# } +#} diff --git a/conf/conf.d/99-local.conf b/conf/conf.d/99-local.conf new file mode 100644 index 0000000..e69de29 diff --git a/conf/conf.d/auth-deny.conf.ext b/conf/conf.d/auth-deny.conf.ext new file mode 100644 index 0000000..35b5c19 --- /dev/null +++ b/conf/conf.d/auth-deny.conf.ext @@ -0,0 +1,15 @@ +# Deny access for users. Included from auth.conf. + +# Users can be (temporarily) disabled by adding a passdb with deny=yes. +# If the user is found from that database, authentication will fail. +# The deny passdb should always be specified before others, so it gets +# checked first. + +# Example deny passdb using passwd-file. You can use any passdb though. +#passdb deny { +# driver = passwd-file +# deny = yes +# +## File contains a list of usernames, one per line +# passwd_file_path = /etc/dovecot/deny-users +#} diff --git a/conf/conf.d/auth-ldap.conf.ext b/conf/conf.d/auth-ldap.conf.ext new file mode 100644 index 0000000..4e5982b --- /dev/null +++ b/conf/conf.d/auth-ldap.conf.ext @@ -0,0 +1,48 @@ +# Authentication for LDAP users. Included from auth.conf. +# +# + +## See + +#ldap_uris = ldap://localhost +#ldap_auth_dn = cn=admin +#ldap_auth_dn_password = supersecret + +#passdb ldap { +# ldap_filter = (&(objectClass=posixAccount)(uid=%{user})) +# ldap_bind = no + +# fields { + # user=%{ldap:uid} + # password=%{ldap:userPassword} + # userdb_home=%{ldap:homeDirectory} + # userdb_uid=%{ldap:uidNumber} + # userdb_gid=%{ldap:gidNumber} +# } +#} + +# "prefetch" user database means that the passdb already provided the +# needed information and there's no need to do a separate userdb lookup. +# +#userdb prefetch { +#} + +#userdb ldap { +# ldap_filter = (&(objectClass=posixAccount)(uid=%{user})) + +# Default fields can be used to specify defaults that LDAP may override +# fields { +# home=/home/virtual/%{user} +# } +#} + +# If you don't have any user-specific settings, you can avoid the userdb LDAP +# lookup by using userdb static instead of userdb ldap, for example: +# +#userdb static { + #fields { + # uid = vmail + # gid = vmail + # home = /var/vmail/%{user} + #} +#} diff --git a/conf/conf.d/auth-master.conf.ext b/conf/conf.d/auth-master.conf.ext new file mode 100644 index 0000000..17839b2 --- /dev/null +++ b/conf/conf.d/auth-master.conf.ext @@ -0,0 +1,12 @@ +# Authentication for master users. Included from auth.conf. + +# By adding master=yes setting inside a passdb you make the passdb a list +# of "master users", who can log in as anyone else. +# + +# Example master user passdb using passwd-file. You can use any passdb though. +#passdb master-passwd-file { +# driver = passwd-file +# master = yes +# passwd_file_path = /etc/dovecot/master-users +#} diff --git a/conf/conf.d/auth-oauth2.conf.ext b/conf/conf.d/auth-oauth2.conf.ext new file mode 100644 index 0000000..ce90f91 --- /dev/null +++ b/conf/conf.d/auth-oauth2.conf.ext @@ -0,0 +1,23 @@ +#auth_mechanisms { +# xoauth2 = yes +# oauthbearer = yes +#} +#oauth2 { +# tokeninfo_url = https://www.googleapis.com/oauth2/v3/tokeninfo?access_token= +# introspection_url = https://www.googleapis.com/oauth2/v2/userinfo +# #force_introspection = yes +# username_attribute = email +#} + +# with local validation +#oauth2 { +# introspection_mode = local +# username_attribute = email +# oauth2_local_validation { +# dict fs { +# fs posix { +# prefix = /etc/dovecot/oauth2-keys/ +# } +# } +# } +#} diff --git a/conf/conf.d/auth-passwdfile.conf.ext b/conf/conf.d/auth-passwdfile.conf.ext new file mode 100644 index 0000000..033f253 --- /dev/null +++ b/conf/conf.d/auth-passwdfile.conf.ext @@ -0,0 +1,20 @@ +# Authentication for passwd-file users. Included from auth.conf. +# +# passwd-like file with specified location. +# + +#passdb passwd-file { +# default_password_scheme = crypt +# auth_username_format = %{user} +# passwd_file_path = /etc/dovecot/users +#} + +#userdb passwd-file { +# auth_username_format = %{user} +# passwd_file_path = /etc/dovecot/users + +# fields { +# quota_rule:default=*:storage=1G +# home=/home/virtual/%{user} +# } +#} diff --git a/conf/conf.d/auth-sql.conf.ext b/conf/conf.d/auth-sql.conf.ext new file mode 100644 index 0000000..4b8f504 --- /dev/null +++ b/conf/conf.d/auth-sql.conf.ext @@ -0,0 +1,180 @@ +# Authentication for SQL users. Included from auth.conf. +# +# + +# For the sql passdb module, you'll need a database with a table that +# contains fields for at least the username and password. If you want to +# use the user@domain syntax, you might want to have a separate domain +# field as well. +# +# If your users all have the same uig/gid, and have predictable home +# directories, you can use the static userdb module to generate the home +# dir based on the username and domain. In this case, you won't need fields +# for home, uid, or gid in the database. +# +# If you prefer to use the sql userdb module, you'll want to add fields +# for home, uid, and gid. Here is an example table: +# +# CREATE TABLE users ( +# username VARCHAR(128) NOT NULL, +# domain VARCHAR(128) NOT NULL, +# password VARCHAR(64) NOT NULL, +# home VARCHAR(255) NOT NULL, +# uid INTEGER NOT NULL, +# gid INTEGER NOT NULL, +# active CHAR(1) DEFAULT 'Y' NOT NULL +# ); + +# Database driver: mysql, pgsql, sqlite +#sql_driver = mysql + +# Database connection string. This is driver-specific setting. +# +# HA / round-robin load-balancing is supported by giving multiple host +# settings, like: host=sql1.host.org host=sql2.host.org +# +# pgsql: +# For available options, see the PostgreSQL documention for the +# PQconnectdb function of libpq. +# Use maxconns=n (default 5) to change how many connections Dovecot can +# create to pgsql. +# +# mysql: +# Basic options emulate PostgreSQL option names: +# host, port, user, password, dbname +# +# But also adds some new settings: +# client_flags - See MySQL manual +# ssl_ca, ssl_ca_path - Set either one or both to enable SSL +# ssl_cert, ssl_key - For sending client-side certificates to server +# ssl_cipher - Set minimum allowed cipher security (default: HIGH) +# option_file - Read options from the given file instead of +# the default my.cnf location +# option_group - Read options from the given group (default: client) +# +# You can connect to UNIX sockets by using host: host=/var/run/mysql.sock +# Note that currently you can't use spaces in parameters. +# +# sqlite: +# The path to the database file. +# +# Examples: +# mysql 192.168.1.1 { +# dbname = users +# } +# mysql sql.example.com { +# ssl = yes +# user = virtual +# password = blarg +# dbname = virtual +# } +# sqlite /etc/dovecot/authdb.sqlite { +# } +# +#mysql /var/run/mysqld/mysqld.sock { +# user = dovecot +# password = dvmail +# dbname = dovecot +#} +#mysql localhost { +# ... +#} + +#passdb sql { +# default_password_scheme = SHA256 + +# passdb query to retrieve the password. It can return fields: +# password - The user's password. This field must be returned. +# user - user@domain from the database. Needed with case-insensitive lookups. +# username and domain - An alternative way to represent the "user" field. +# +# The "user" field is often necessary with case-insensitive lookups to avoid +# e.g. "name" and "nAme" logins creating two different mail directories. If +# your user and domain names are in separate fields, you can return "username" +# and "domain" fields instead of "user". +# +# The query can also return other fields which have a special meaning, see +# https://doc.dovecot.org/latest/core/config/auth/passdb.html#extra-fields +# +# Commonly used available substitutions (see https://doc.dovecot.org/latest/core/settings/variables.html +# for full list): +# %{user} = entire user@domain +# %{user|username} = user part of user@domain +# %{user|domain} = domain part of user@domain +# +# Note that these can be used only as input to SQL query. If the query outputs +# any of these substitutions, they're not touched. Otherwise it would be +# difficult to have eg. usernames containing '%' characters. +# +# Example: +# query = SELECT userid AS user, pw AS password \ +# FROM users WHERE userid = '%u' AND active = 'Y' +# +# query = \ +# SELECT userid as username, domain, password \ +# FROM users WHERE userid = '%{user|username}' AND domain = '%{user|domain}' +#} + +#userdb sql { +# userdb query to retrieve the user information. It can return fields: +# uid - System UID (overrides mail_uid setting) +# gid - System GID (overrides mail_gid setting) +# home - Home directory +# mail_driver - Mail driver +# mail_path - Mail storage path +# +# None of these are strictly required. If you use a single UID and GID, and +# home or mail directory fits to a template string, you could use userdb static +# instead. For a list of all fields that can be returned, see +# Examples: +# query = SELECT home, uid, gid FROM users WHERE userid = '%{user}' +# query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%{user}' +# query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%{user}' +# +# query = \ +# SELECT home, uid, gid \ +# FROM users WHERE userid = '%{user|username}' AND domain = '%{user|domain}' + +# Query to get a list of all usernames. +# iterate_query = SELECT username AS user,domain FROM users + +# userdb_ldap { +# iterate_fields { +# home = /var/vmail/%{home} +# } +# } +#} + +#passdb static { +# fields { +# user=%{user|username|lower} +# noauthenticate=yes +# } +## you can remove next line if you want to always normalize your usernames +# skip = authenticated +#} + +# "prefetch" user database means that the passdb already provided the +# needed information and there's no need to do a separate userdb lookup. +# +#userdb prefetch { +#} + +#userdb static { +# fields { +# user=%{user|lower} +# } +# you can remove next line if you want to always normalize your usernames +# skip = found +#} + +# If you don't have any user-specific settings, you can avoid the user_query +# by using userdb static instead of userdb sql, for example: +# +#userdb static { + #fields { + # uid = vmail + # gid = vmail + # home = /var/vmail/%{user} + #} +#} diff --git a/conf/conf.d/auth-static.conf.ext b/conf/conf.d/auth-static.conf.ext new file mode 100644 index 0000000..d8ddb54 --- /dev/null +++ b/conf/conf.d/auth-static.conf.ext @@ -0,0 +1,35 @@ +# Static passdb. Included from auth.conf. + +# This can be used for situations where Dovecot doesn't need to verify the +# username or the password, or if there is a single password for all users: +# +# - proxy frontend, where the backend verifies the password +# - proxy backend, where the frontend already verified the password +# - authentication with SSL certificates +# - simple testing +# +# Each passdb must have unique name, but if you onlu use it once per driver +# you can use driver name as passdb/userdb section name. + +#passdb static { +# fields { +# proxy = y +# host = %{user|md5|substr(1)}.example.com +# nopassword = y +# } +#} + +#passdb static2 { +# driver = static +# fields { +# password = test +# } +#} + +#userdb static { +# fields { +# uid = vmail +# gid = vmail +# home = /home/%{user} +# } +#} diff --git a/conf/conf.d/auth-system.conf.ext b/conf/conf.d/auth-system.conf.ext new file mode 100644 index 0000000..237db61 --- /dev/null +++ b/conf/conf.d/auth-system.conf.ext @@ -0,0 +1,72 @@ +# Authentication for system users. Included from auth.conf. +# +# +# + +# Driver is only needed if the section name is not same as driver's name. + +# PAM authentication. Preferred nowadays by most systems. +# PAM is typically used with either userdb passwd or userdb static. +# REMEMBER: You'll need /etc/pam.d/dovecot file created for PAM +# authentication to actually work. +passdb pam { +# driver = pam +# session = yes +# setcred = yes +# failure_show_msg = yes +# max_requests = 20 +# service_name = dovecot +# skip = authenticated +# fields { +# } +} + +userdb passwd { +} + +# System users (NSS, /etc/passwd, or similar). +# In many systems nowadays this uses Name Service Switch, which is +# configured in /etc/nsswitch.conf. +#passdb passwb { +#} + +# PAM-like authentication for OpenBSD. +# +#passdb bsdauth { +#} + +## +## User databases +## + +# System users (NSS, /etc/passwd, or similiar). In many systems nowadays this +# uses Name Service Switch, which is configured in /etc/nsswitch.conf. +#userdb passwd-file { + #driver = passwd-file + #auth_username_format=%{user|lower} + #passwd_file_path = /etc/passwd + #fields { + # user= %{user|lower} + # name = %{user|lower} + # home = /var/vmail/%{user} + #} + #skip = found +#} + +# Static settings generated from template +#userdb static { + #driver = static + # Can return anything a userdb could normally return. For example: + # fields { + # uid = 500 + # gid = 500 + # home = /var/mail/%{user} + # } + # LDA and LMTP needs to look up users only from the userdb. This of course + # doesn't work with static userdb because there is no list of users. + # Normally static userdb handles this by doing a passdb lookup. This works + # with most passdbs, with PAM being the most notable exception. If you do + # the user verification another way, you can add allow_all_users=yes + # in which case the passdb lookup is skipped. + #allow_all_users = yes +#} diff --git a/conf/dovecot.conf b/conf/dovecot.conf new file mode 100644 index 0000000..ee7eb33 --- /dev/null +++ b/conf/dovecot.conf @@ -0,0 +1,87 @@ +## Dovecot configuration file + +# If you're in a hurry, see https://doc.dovecot.org/latest/core/config/guides/quick.html + +# "doveconf -n" command gives a clean output of the changed settings. Use it +# instead of copy&pasting files when posting to the Dovecot mailing list. + +# '#' character and everything after it is treated as comments. Extra spaces +# and tabs are ignored. If you want to use either of these explicitly, put the +# value inside quotes, eg.: key = "# char and trailing whitespace " + +# Default values are shown for each setting, it's not required to uncomment +# those. These are exceptions to this though: No sections (e.g. namespace {}) +# or plugin settings are added by default, they're listed only as examples. +# Paths are also just examples with the real defaults being based on configure +# options. The paths listed here are for configure --prefix=/usr/local +# --sysconfdir=/usr/local/etc --localstatedir=/var + +dovecot_config_version = 2.4.0 +dovecot_storage_version = 2.4.0 + +# Protocols we want to be serving. +#protocols = imap pop3 lmtp +!include_try /usr/share/dovecot/protocols.d/*.protocol + +# A comma separated list of IPs or hosts where to listen in for connections. +# "*" listens in all IPv4 interfaces, "::" listens in all IPv6 interfaces. +# If you want to specify non-default ports or anything more complex, +# edit conf.d/master.conf. +#listen = *, :: + +# Base directory where to store runtime data. +#base_dir = /var/run/dovecot/ + +# Name of this instance. In multi-instance setup doveadm and other commands +# can use -i to select which instance is used (an alternative +# to -c ). The instance name is also added to Dovecot processes +# in ps output. +#instance_name = dovecot + +# Greeting message for clients. +#login_greeting = Dovecot ready. + +# Space separated list of trusted network ranges. Connections from these +# IPs are allowed to override their IP addresses and ports (for logging and +# for authentication checks). disable_plaintext_auth is also ignored for +# these networks, unless ssl=required. +# Typically you'd specify your IMAP proxy servers here. +#login_trusted_networks = + +# With proxy_maybe=yes if proxy destination matches any of these IPs, don't do +# proxying. This isn't necessary normally, but may be useful if the destination +# IP is e.g. a load balancer's IP. +#auth_proxy_self = + +# Show more verbose process titles (in ps). Currently shows user name and +# IP address. Useful for seeing who are actually using the IMAP processes +# (eg. shared mailboxes or if same uid is used for multiple accounts). +#verbose_proctitle = yes + +# Should all processes be killed when Dovecot master process shuts down. +# Setting this to "no" means that Dovecot can be upgraded without +# forcing existing client connections to close (although that could also be +# a problem if the upgrade is e.g. because of a security fix). +#shutdown_clients = yes + +# If non-zero, run mail commands via this many connections to doveadm server, +# instead of running them directly in the same process. +#doveadm_worker_count = 0 +# UNIX socket or host:port used for connecting to doveadm server +#doveadm_socket_path = doveadm-server + +# Space separated list of environment variables that are preserved on Dovecot +# startup and passed down to all of its child processes. You can also give +# key=value pairs to always set specific settings. +#import_environment { +# TZ=%{env:TZ} +#} + +# Most of the actual configuration gets included below. The filenames are +# first sorted by their ASCII value and parsed in that order. The 00-prefixes +# in filenames are intended to make it easier to understand the ordering. +!include conf.d/*.conf + +# A config file can also tried to be included without giving an error if +# it's not found: +!include_try local.conf diff --git a/conf/local.conf b/conf/local.conf new file mode 100644 index 0000000..e69de29 diff --git a/control b/control new file mode 100644 index 0000000..5ae13e3 --- /dev/null +++ b/control @@ -0,0 +1,304 @@ +Source: dovecot +Section: mail +Priority: optional +Maintainer: Dovecot Maintainers +Uploaders: Noah Meyerhans +Build-Depends: debhelper-compat (= 13), + default-libmysqlclient-dev, + krb5-multidev, + libapparmor-dev [linux-any], + libbz2-dev, + libcap-dev [linux-any], + libcdb-dev, + libdb-dev, + libexpat-dev, + libexttextcat-dev, + libicu-dev, + libldap2-dev, + liblua5.4-dev, + liblz4-dev, + liblzma-dev, + libpam0g-dev, + libpq-dev, + libsasl2-dev, + libsodium-dev, + libsqlite3-dev, + libssl-dev, + libstemmer-dev, + libsystemd-dev [linux-any], + libtirpc-dev, + libunwind-dev, + libwrap0-dev, + libxapian-dev, + libzstd-dev, + lsb-release, + lua-json , + pkgconf, + zlib1g-dev +Standards-Version: 4.7.0 +Rules-Requires-Root: no +Homepage: https://dovecot.org/ +Vcs-Git: https://salsa.debian.org/debian/dovecot.git +Vcs-Browser: https://salsa.debian.org/debian/dovecot + +Package: dovecot-core +Architecture: any +Pre-Depends: ${misc:Pre-Depends} +Depends: adduser, + libpam-runtime, + openssl, + ssl-cert, + ucf, + ${misc:Depends}, + ${shlibs:Depends} +Suggests: dovecot-flatcurve, + dovecot-gssapi, + dovecot-imapd, + dovecot-ldap, + dovecot-lmtpd, + dovecot-managesieved, + dovecot-mysql, + dovecot-pgsql, + dovecot-pop3d, + dovecot-sieve, + dovecot-solr, + dovecot-sqlite, + dovecot-submissiond, + ntp +Provides: dovecot-abi-${dovecot:ABI-Version}, dovecot-common +Replaces: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4) +Breaks: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4) +Description: secure POP3/IMAP server - core files + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package contains the Dovecot main server and its command line utility. + +Package: dovecot-dev +Architecture: any +Depends: ${misc:Depends}, ${shlibs:Depends} +Replaces: dovecot-common (<< 1:2.0.14-2~), dovecot-core (<< 1:2.2.26.0-1~) +Breaks: dovecot-common (<< 1:2.0.14-2~), dovecot-core (<< 1:2.2.26.0-1~) +Description: secure POP3/IMAP server - header files + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package contains header files needed to compile plugins for the Dovecot + mail server. + +Package: dovecot-imapd +Architecture: any +Depends: dovecot-core (= ${binary:Version}), + ucf, + ${misc:Depends}, + ${shlibs:Depends} +Suggests: ufw +Provides: imap-server +Description: secure POP3/IMAP server - IMAP daemon + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package contains the Dovecot IMAP server. + +Package: dovecot-pop3d +Architecture: any +Depends: dovecot-core (= ${binary:Version}), + ucf, + ${misc:Depends}, + ${shlibs:Depends} +Suggests: ufw +Provides: pop3-server +Description: secure POP3/IMAP server - POP3 daemon + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package contains the Dovecot POP3 server. + +Package: dovecot-lmtpd +Architecture: any +Depends: dovecot-core (= ${binary:Version}), + ucf, + ${misc:Depends}, + ${shlibs:Depends} +Replaces: dovecot-common (<< 1:2.0.14-2~) +Breaks: dovecot-common (<< 1:2.0.14-2~) +Description: secure POP3/IMAP server - LMTP server + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package contains the Dovecot LMTP server. + +Package: dovecot-managesieved +Architecture: any +Depends: dovecot-core (= ${binary:Version}), + dovecot-sieve (= ${binary:Version}), + ucf, + ${misc:Depends}, + ${shlibs:Depends} +Replaces: dovecot-common (<< 1:2.0.14-2~) +Breaks: dovecot-common (<< 1:2.0.14-2~) +Description: secure POP3/IMAP server - ManageSieve server + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package contains the Dovecot ManageSieve server. + +Package: dovecot-pgsql +Architecture: any +Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends} +Description: secure POP3/IMAP server - PostgreSQL support + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package provides PostgreSQL support for Dovecot. + +Package: dovecot-mysql +Architecture: any +Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends} +Description: secure POP3/IMAP server - MySQL support + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package provides MySQL support for Dovecot. + +Package: dovecot-sqlite +Architecture: any +Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends} +Description: secure POP3/IMAP server - SQLite support + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package provides SQLite support for Dovecot. + +Package: dovecot-ldap +Architecture: any +Depends: dovecot-core (= ${binary:Version}), + ucf, + ${misc:Depends}, + ${shlibs:Depends} +Replaces: dovecot-core (<< 1:2.2.26.0-1~) +Breaks: dovecot-core (<< 1:2.2.26.0-1~) +Description: secure POP3/IMAP server - LDAP support + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package provides LDAP support for Dovecot. + +Package: dovecot-gssapi +Architecture: any +Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends} +Description: secure POP3/IMAP server - GSSAPI support + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package provides GSSAPI authentication support for Dovecot. + +Package: dovecot-sieve +Architecture: any +Depends: dovecot-core (= ${binary:Version}), + ucf, + ${misc:Depends}, + ${shlibs:Depends} +Breaks: dovecot-core (<= 1:2.2.13-6), dovecot-imapd (<< 1:2.2.26.0-1~) +Replaces: dovecot-core (<= 1:2.2.13-6), dovecot-imapd (<< 1:2.2.26.0-1~) +Description: secure POP3/IMAP server - Sieve filters support + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package provides Sieve filters support for Dovecot. + +Package: dovecot-flatcurve +Architecture: any +Conflicts: dovecot-fts-flatcurve +Replaces: dovecot-fts-flatcurve +Depends: dovecot-core (= ${binary:Version}), + ucf, + ${misc:Depends}, + ${shlibs:Depends} +Description: secure POP3/IMAP server - Flatcurve support + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package provides Flatcurve full text search support for Dovecot. + +Package: dovecot-solr +Architecture: any +Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends} +Description: secure POP3/IMAP server - Solr support + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package provides Solr full text search support for Dovecot. + +Package: dovecot-submissiond +Architecture: any +Depends: dovecot-core (= ${binary:Version}), + ucf, + ${misc:Depends}, + ${shlibs:Depends} +Breaks: dovecot-submission (<< 1:2.3.1-2~) +Replaces: dovecot-submission (<< 1:2.3.1-2~) +Description: secure POP3/IMAP server - mail submission agent + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package contains the Dovecot Mail Submission Agent which implements a + basic SMTP submission service with BURL support. + +Package: dovecot-auth-lua +Architecture: any +Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends} +Description: secure POP3/IMAP server - Lua authentication plugin + Dovecot is a mail server whose major goals are security and extreme + reliability. It tries very hard to handle all error conditions and verify + that all data is valid, making it nearly impossible to crash. It supports + mbox/Maildir and its own dbox/mdbox formats, and should also be pretty + fast, extensible, and portable. + . + This package contains an authentication plugin allowing password and user + databases to be implemented in Lua. diff --git a/copyright b/copyright new file mode 100644 index 0000000..8ede253 --- /dev/null +++ b/copyright @@ -0,0 +1,266 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: dovecot +Upstream-Contact: Timo Sirainen + Dovecot Mailing List +Comment: This package was debianized by Jaldhar H. Vyas on + Tue, 3 Dec 2002 01:10:07 -0500. +Source: https://github.com/dovecot/core +Files-Excluded-pigeonhole: doc/rfc + +Files: * +Copyright: (c) 2001-2022 Dovecot authors +Comment: see AUTHORS +License: LGPL-2.1 + +Files: src/lib/* + src/lib-sql/* +Copyright: (c) 2001-2022 Dovecot authors +Comment: see AUTHORS +License: MIT + +Files: src/auth/* +Copyright: (c) 2004-2006 Andrey Panin + (c) 2005 Jelmer Vernooij + (c) 2011-2016 Florian Zeitz + (c) 2007 Dmitry Butskoy + (C) 2006 Simon L Jackson + (c) 2015-2017 Dovecot Oy +License: MIT + +Files: src/lib-auth/password-scheme-md5crypt.c +Copyright: Poul-Henning Kamp +License: Beerware + "THE BEER-WARE LICENSE" (Revision 42): + wrote this file. As long as you retain this notice you + can do whatever you want with this stuff. If we meet some day, and you think + this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp + +Files: src/lib-otp/* +Copyright: (c) 2004-2006 Andrey Panin + (c) Copyright 1997-2005 Simon Tatham +License: MIT + +Files: src/lib/md5.c +Copyright: N/A +License: public-domain-solardiz + Written by Solar Designer in 2001, and placed in the + public domain. There's absolutely no warrant. + +Files: src/lib/sha1.* +Copyright: Copyright (C) 1995, 1996, 1997, and 1998 WIDE Project. +License: BSD-3-Clause + +Files: src/lib/sha2.* src/lib/sha3.* +Copyright: Copyright (C) 2005, 2007 Olivier Gay +License: BSD-3-Clause + +Files: src/lib/byteorder.h src/lib/test-byteorder.c +Copyright: (c) 2016-2017 Josef 'Jeff' Sipek +License: MIT + +Files: src/lib/hmac.* +Copyright: (c) 2004 Andrey Panin + (c) 2011-2016 Florian Zeitz +License: MIT + +Files: src/lib/hmac-cram-md5.c +Copyright: (c) 2003 Joshua Goodall +License: MIT + +Files: src/lib/ioloop-iolist.c +Copyright: (c) 2004 Andrey Panin +License: MIT + +Files: src/lib/ioloop-kqueue.c src/lib/ioloop-notify-kqueue.c +Copyright: (c) 2005 Vaclav Haisman +License: MIT + +Files: src/lib/UnicodeData.txt +Copyright: Copyright (C) 1991-2007 Unicode, Inc. +License: Unicode-DFS-2015 + +Files: src/lib-language/stopwords/* +Copyright: Copyright (c) 2001, Dr Martin Porter + Copyright (c) 2002, Richard Boulton. +Comment: License obtained from http://snowball.tartarus.org/license.html +License: BSD-3-Clause + +Files: src/lib-language/stopwords/stopwords_en.txt +Copyright: Apache Software Foundation +License: Apache-2.0 + +Files: src/lib-language/stopwords/stopwords_ro.txt +Copyright: (c) 2005, Jacques Savoy. +License: BSD-2-Clause + +Files: debian/* +Copyright: (c) 2006-2016 Jaldhar H. Vyas + (c) 2006-2009 Fabio Tranchitella + (c) 2008-2009 Joel Johnson + (c) 2009-2012 Marco Nenciarini + (c) 2011-2013 Micah Anderson + (c) 2014-2015 Jelmer Vernooij + (c) 2016-2018 Apollon Oikonomopoulos + (c) 2020-2022 Noah Meyerhans + (c) 2020-2022 Christian Göttsche +License: GPL-2+ + +Files: pigeonhole/* +Copyright: (c) 2002-2021 Stephan Bosch + (c) 2002-2021 Dovecot authors +Comment: Source obtained from https://pigeonhole.dovecot.org/ +License: LGPL-2.1 + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + +License: LGPL-2.1 + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + . + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + . + You should have received a copy of the GNU Lesser General Public + License along with this program. If not, see + . + . + On Debian systems, the full text of the GNU Lesser General Public License can + be found at /usr/share/common-licenses/LGPL-2.1. + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + On Debian systems, the full text of the Apache License, Version 2.0 can be + found at /usr/share/common-licenses/Apache-2.0. + +License: BSD-3-Clause + All rights reserved. + . + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +License: BSD-2-Clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + . + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + . + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: GPL-2+ + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . + . + On Debian systems, the full text of the GNU General Public License version 2 + can be found at /usr/share/common-licenses/GPL-2. + +License: Unicode-DFS-2015 + Copyright © 1991-2015 Unicode, Inc. All rights reserved. Distributed under the + Terms of Use in http://www.unicode.org/copyright.html. + . + Permission is hereby granted, free of charge, to any person obtaining a copy + of the Unicode data files and any associated documentation (the "Data Files") + or Unicode software and any associated documentation (the "Software") to deal + in the Data Files or Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, and/or sell copies + of the Data Files or Software, and to permit persons to whom the Data Files or + Software are furnished to do so, provided that + . + (a) this copyright and permission notice appear with all copies of the Data + Files or Software, + (b) this copyright and permission notice appear in associated documentation, + and + (c) there is clear notice in each modified Data File or in the Software as + well as in the documentation associated with the Data File(s) or Software + that the data or software has been modified. + . + THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY + KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD + PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN + THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL + DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR + SOFTWARE. + . + Except as contained in this notice, the name of a copyright holder shall not + be used in advertising or otherwise to promote the sale, use or other dealings + in these Data Files or Software without prior written authorization of the + copyright holder. diff --git a/dh.pem b/dh.pem new file mode 100644 index 0000000..a734b90 --- /dev/null +++ b/dh.pem @@ -0,0 +1,13 @@ +-----BEGIN DH PARAMETERS----- +MIICCAKCAgEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxOb +IlFKCHmONATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjft +awv/XLb0Brft7jhr+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT +mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhgh +fDKQXkYuNs474553LBgOhgObJ4Oi7Aeij7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq +5RXSJhiY+gUQFXKOWoqqxC2tMxcNBFB6M6hVIavfHLpk7PuFBFjb7wqK6nFXXQYM +fbOXD4Wm4eTHq/WujNsJM9cejJTgSiVhnc7j0iYa0u5r8S/6BtmKCGTYdgJzPshq +ZFIfKxgXeyAMu+EXV3phXWx3CYjAutlG4gjiT6B05asxQ9tb/OD9EI5LgtEgqSEI +ARpyPBKnh+bXiHGaEL26WyaZwycYavTiPBqUaDS2FQvaJYPpyirUTOjbu8LbBN6O ++S6O/BQfvsqmKHxZR05rwF2ZspZPoJDDoiM7oYZRW+ftH2EpcM7i16+4G912IXBI +HNAGkSfVsFqpk7TqmI2P3cGG/7fckKbAj030Nck0BjGZ//////////8CAQI= +-----END DH PARAMETERS----- diff --git a/dovecot-core.NEWS b/dovecot-core.NEWS new file mode 100644 index 0000000..c4e6a5f --- /dev/null +++ b/dovecot-core.NEWS @@ -0,0 +1,193 @@ +dovecot (1:2.4.1+dfsg1-1) unstable; urgency=medium + + Dovecot 2.4 is a major upgrade from the previous 2.3 branch and introduces + an incompatible configuration language. Please see + https://doc.dovecot.org/2.4.0/installation/upgrade/2.3-to-2.4.html for + details on transitioning to the new language. The default configuration + shipped by Debian has been updated for compatibility with the new + configuration, but any locally maintained configuration will need to be + updated. + + -- Noah Meyerhans Mon, 10 Mar 2025 13:28:20 -0400 + +dovecot (1:2.3.2-1) unstable; urgency=medium + + Upgrading to the 2.3 series may require manual configuration changes. + Some settings have been removed, while others have had their defaults + changed. Please see + + /usr/share/doc/dovecot-core/wiki/Upgrading.2.3.txt.gz + + or the online version at + + https://wiki2.dovecot.org/Upgrading/2.3 + + for more information and review your configuration accordingly. + + -- Apollon Oikonomopoulos Sat, 24 Mar 2018 00:34:07 +0200 + +dovecot (1:2.2.31-1) unstable; urgency=medium + + TLS is now enabled by default, using the ssl-cert-snakeoil certificate + provided by the ssl-cert package. Upgrades from older versions will be + prompted to accept the new configuration and enable TLS. If you have already + configured TLS yourself, you'll most probably want to keep your settings + intact. + + See /usr/share/doc/dovecot-core/README.Debian for more information on the + certificate's default location and how to install your own certificates. + + -- Apollon Oikonomopoulos Sun, 25 Jun 2017 01:09:28 +0300 + +dovecot (1:2.2.21-1) unstable; urgency=medium + + This release disables the dovecot.socket systemd unit by default. The unit is + disabled only if the dovecot.service unit is already enabled, making sure + that dovecot will start on system boot. If you are upgrading dovecot and + previously relied on dovecot.socket and dovecot.service being both enabled, + please re-enable dovecot.socket manually using + + systemctl enable dovecot.socket + + Future package updates will not disable the socket unit again. For details + regarding this decision, please see Debian bugs #803915 and #814999. + + -- Apollon Oikonomopoulos Fri, 19 Feb 2016 16:54:27 +0200 + +dovecot (1:2.1.7-7) unstable; urgency=high + + If you are upgrading from stable or the earlier 2.1.7 packages in testing, + you are advised to backup your configuration files before continuing. + Carefully read /usr/share/doc/dovecot-core/README.Debian.gz for important + information about problems that might affect your upgrade. + + -- Jaldhar H. Vyas Mon, 04 Feb 2013 16:27:17 -0500 + +dovecot (1:2.0.5-1) experimental; urgency=low + + * A lot of settings have changed. Dovecot v2.0 can still use most of + the v1.x configuration files, but it logs a lot of warnings at + startup. A quick and easy way to convert your old config file to + v2.0 format is: + + doveconf -n -c dovecot-1.conf > dovecot-2.conf + + This command logs a warning about each obsolete setting it + converts to the new format. You can either go through the warnings + to figure out what changes exactly were done, or you can simply + trust doveconf and replace your old config with the newly + generated one. + + * Dovecot uses two system users for internal purposes now by + default: "dovenull" and "dovecot". "dovenull" user is used by + completely untrustworthy processes, while "dovecot" user is used + for slightly more trusted processes. + + * In conf.d/10-mail.conf, the default mail_location is set to + mbox:~/mail:INBOX=/var/mail/%u + This has to be set because Dovecots auto-detection of mailboxes fails + when a user has no mail. I tried to provide a reasonable default + but advanced users will certainly want to change this. + + Related to this, you might see an error in the logs about dovecot not + being able to chgrp certain files. This is because the default user + mailboxes in Debian belong to group mail which dovecot is not part of + (nor should be.) The Debian package has been patched so that the error + will not be fatal but for a more permanent solution, see here: + http://wiki2.dovecot.org/Errors/ChgrpNoPerm for some discussion. Or + you can set mail_location (and the equivalent settings in your MDA) so + that mailboxes are in the users home directory. + + * No more convert plugin, use dsync instead + + * No more expire-tool, use doveadm expunge instead. Also expire + configuration is different. + + * Post-login scripts are configured differently and need to be modified + + * Quota warnings are configured differently and the script may need + to be modified (most environment settings like $USER are gone) + + * Global ACL filenames now require namespace prefix (e.g. if you use + "INBOX." prefix, /etc/acls/foo needs to be renamed to + /etc/acls/INBOX.foo + + * Maildir: Permissions for newly created mail files are no longer + copied from dovecot-shared file, but instead from the mail + directory (e.g. for "foo" mailbox, they're taken from + ~/Maildir/.foo directory) + + * dbox: v2.0 format is slightly different, but backwards + compatible. The main problem is that v2.0 no longer supports + maildir-dbox hybrid resulting from "fast Maildir migration". If + you have any Maildir files in your dbox, you need to convert them + somehow (some examples). You might also consider using dsync to + get rid of the old unused metadata in your dbox files. + + + * Pre-login and post-login CAPABILITY reply is now + different. Dovecot expects clients to recognize new automatically + sent capabilities. This should work with all commonly used + clients, but some rarely used clients might have problems. Either + get the client fixed, or set imap_capability manually. + + * ManageSieve protocol was assigned an official port by IANA: + 4190. This is used by Pigeonhole by default now. If you want to + listen also on the old 2000 port, see the example in + /usr/share/doc/dovecot-common/wiki/ManageSieve.Configuration.txt.gz + + * deliver binary was renamed to dovecot-lda (but a symlink still + exists for now) + + * deliver -n parameter was replaced by lda_mailbox_autocreate + setting. The default also changed to "no". + + * deliver -s parameter was replaced by lda_mailbox_autosubscribe + setting. The default is "no", as before. + + -- Marco Nenciarini Mon, 18 Oct 2010 12:59:14 +0200 + +dovecot (1:1.0.12-1) unstable; urgency=low + + * mail_extra_groups=mail setting is often used insecurely to give Dovecot + access to create dotlocks to /var/mail directory. The setting has been + replaced by mail_privileged_group and mail_access_groups. + Read also: http://dovecot.org/list/dovecot/2008-March/029196.html + + -- Fabio Tranchitella Thu, 06 Mar 2008 15:46:03 +0100 + +dovecot (1.0.beta3-1) unstable; urgency=low + + * Starting from this release, dovecot-lda is included in dovecot-common. + The previous executable deliver has been removed from the upstream + source package. + * Other new features recently added include + + vpopmail support + + quota support + + GSSAPI support + * All these new features mean there are some configuration file changes + please review the default /etc/dovecot/dovecot.conf and merge in any + new bits. (If you don't use any new features, your configuration should + remain compatible.) + + -- Fabio Tranchitella Fri, 17 Feb 2006 17:05:37 +0000 + +dovecot (1.0.alpha4-1) unstable; urgency=low + + * This is the 1.0alpha branch. Once again there have been incompatible + changes to the syntax of /etc/dovecot/dovecot.conf. + * The dovecot-common package now includes the zlib plugin for compressed + mboxen. Make sure you have mail_use_modules=yes in imap and/or pop3 + section in /etc/dovecot/dovecot.conf if you want to use this. + + -- Jaldhar H. Vyas Wed, 20 Jul 2005 06:30:37 -0400 + +dovecot (0.99.20050712-1) unstable; urgency=low + + * This is the 1.0stable development branch. There have been major + changes and new features have been added so check your configuration + carefully. In particular, /etc/dovecot/dovecot-mysql.conf and + /etc/dovecot/dovecot-pgsql.conf have been replaced by + /etc/dovecot/dovecot-sql.conf . + + -- Jaldhar H. Vyas Wed, 20 Jul 2005 06:30:37 -0400 diff --git a/dovecot-core.README.Debian b/dovecot-core.README.Debian new file mode 100644 index 0000000..e4fc73c --- /dev/null +++ b/dovecot-core.README.Debian @@ -0,0 +1,35 @@ +Dovecot Debian package +====================== + +Please refer to http://wiki2.dovecot.org for the full documentation of this +package and its configuration. + +Note that the default value for mbox_write_locks differs between Debian and +upstream Dovecot: + Debian: mbox_write_locks = "fnctl dotlock" + Dovecot: mbox_write_locks = "dotlock fnctl" + +TLS setup +--------- + +TLS is enabled by default on new installations. Dovecot will use the +self-signed certificate provided by ssl-cert-snakeoil by default. The +certificate and key are accessed via symbolic links in /etc/dovecot/private. + +If you wish to use your own certificates, feel free to replace the symbolic +links in /etc/dovecot/private with the actual certificate/key of your +preference, or place the certificate and key at the location of your preference +and update /etc/dovecot/conf.d/10-ssl.conf accordingly. + +dovenull group +-------------- + +Upstream recommends that for security's sake, the dovenull user should belong +to its own private group to which no one else belongs. Prior to 2.2.9-1, the +dovenull user was in the nogroup group which contains other users. This has +been rectified for new installs but upgraders should make the change +themselves like this: + +# addgroup --system --group dovenull +# usermod -g dovenull dovenull + diff --git a/dovecot-core.bug-control b/dovecot-core.bug-control new file mode 100644 index 0000000..0002beb --- /dev/null +++ b/dovecot-core.bug-control @@ -0,0 +1 @@ +package-status: dovecot-common dovecot-core dovecot-dev dovecot-gssapi dovecot-imapd dovecot-ldap dovecot-lmtpd dovecot-managesieved dovecot-mysql dovecot-pgsql dovecot-pop3d dovecot-sieve dovecot-sqlite diff --git a/dovecot-core.bug-script b/dovecot-core.bug-script new file mode 100644 index 0000000..1bf1d19 --- /dev/null +++ b/dovecot-core.bug-script @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e + +pecho() { + echo "$@" + echo "$@"|sed 's/./-/g' +} + + +if [ -x /usr/bin/doveconf ] +then + echo + cat <<-EOF + You can append "doveconf -n" output to the bug report. It will help the + maintainers to better understand your configuration. + It normally doesn't contain any sensitive information, but you should + review it before sending. + EOF + echo + yesno "Do you want to append \"doveconf -n\" output to your bug report? " yep + if [ "$REPLY" == yep ] + then + echo >&3 + pecho "dovecot configuration" >&3 + /usr/bin/doveconf -n >&3 + fi +fi diff --git a/dovecot-core.dirs b/dovecot-core.dirs new file mode 100644 index 0000000..b91716f --- /dev/null +++ b/dovecot-core.dirs @@ -0,0 +1,4 @@ +etc/dovecot/conf.d +etc/dovecot/private +usr/share/dovecot/protocols.d +var/lib/dovecot diff --git a/dovecot-core.docs b/dovecot-core.docs new file mode 100644 index 0000000..166765a --- /dev/null +++ b/dovecot-core.docs @@ -0,0 +1,2 @@ +NEWS +TODO diff --git a/dovecot-core.dovecot.default b/dovecot-core.dovecot.default new file mode 100644 index 0000000..dee9239 --- /dev/null +++ b/dovecot-core.dovecot.default @@ -0,0 +1,4 @@ +# /etc/default/dovecot + +# Set to '1' to allow Dovecot daemons to produce core dumps +#ALLOW_COREDUMPS=1 diff --git a/dovecot-core.dovecot.init b/dovecot-core.dovecot.init new file mode 100644 index 0000000..730c323 --- /dev/null +++ b/dovecot-core.dovecot.init @@ -0,0 +1,185 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: dovecot +# Required-Start: $local_fs $remote_fs $network $syslog $time +# Required-Stop: $local_fs $remote_fs $network $syslog +# Should-Start: postgresql mysql slapd winbind nslcd +# Should-Stop: postgresql mysql slapd winbind nslcd +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Dovecot init script +# Description: Init script for dovecot services +### END INIT INFO + +# Author: Miquel van Smoorenburg . +# Modified for Debian GNU/Linux +# by Ian Murdock . +# + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin +DESC="IMAP/POP3 mail server" +NAME=dovecot +DAEMON=/usr/sbin/dovecot +DAEMON_ARGS="" +SCRIPTNAME=/etc/init.d/$NAME +CONF=/etc/dovecot/${NAME}.conf + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Exit if the configuration file doesn't exist +[ -f "$CONF" ] || exit 0 + +# Exit if explicitly told to +[ "$ENABLED" != "0" ] || exit 0 + +# Allow core dumps if requested +[ "$ALLOW_COREDUMPS" != "1" ] || ulimit -c unlimited + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# conf file readable? +if [ ! -r ${CONF} ]; then + log_daemon_msg "${CONF}: not readable" "$NAME" && log_end_msg 1; + exit 1; +fi + +# dont check for inetd.conf if its not installed +if [ -f /etc/inetd.conf ]; then + # The init script should do nothing if dovecot or another imap/pop3 server + # is being run from inetd, and dovecot is configured to run as an imap or + # pop3 service + for p in `sed -r "s/^ *(([^:]+|\[[^]]+]|\*):)?(pop3s?|imaps?)[ \t].*/\3/;t;d" \ + /etc/inetd.conf` + do + for q in `doveconf -n -h protocols` + do + if [ $p = $q ]; then + log_daemon_msg "protocol ${p} configured both in inetd and in dovecot" "$NAME" && log_end_msg 1 + exit 0 + fi + done + done +fi + +# determine the location of the PID file +# override by setting base_dir in conf file or PIDBASE in /etc/defaults/$NAME +PIDBASE=${PIDBASE:-`doveconf -n -c ${CONF} -h base_dir`} +PIDFILE=${PIDBASE:-/var/run/dovecot}/master.pid + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test -- -c ${CONF} > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- -c ${CONF} \ + $DAEMON_ARGS \ + || return 2 +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name ${DAEMON##*/} + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --pidfile $PIDFILE --name ${DAEMON##*/} + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal HUP --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + + +case "$1" in + start) + log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) log_end_msg 0 ;; + 2) log_end_msg 1 ;; + esac + ;; + stop) + log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) log_end_msg 0 ;; + 2) log_end_msg 1 ;; + esac + ;; + reload|force-reload) + log_daemon_msg "Reloading $DESC" "$NAME" + do_reload + log_end_msg $? + ;; + restart) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + status) + status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $? + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload|status}" >&2 + exit 3 + ;; +esac diff --git a/dovecot-core.dovecot.pam b/dovecot-core.dovecot.pam new file mode 100644 index 0000000..3443434 --- /dev/null +++ b/dovecot-core.dovecot.pam @@ -0,0 +1,6 @@ +#%PAM-1.0 + +@include common-auth +@include common-account +@include common-session + diff --git a/dovecot-core.install b/dovecot-core.install new file mode 100644 index 0000000..bbbaf19 --- /dev/null +++ b/dovecot-core.install @@ -0,0 +1,3 @@ +debian/dh.pem usr/share/dovecot +debian/dovecot-openssl.cnf usr/share/dovecot +debian/maildirmake.dovecot usr/bin diff --git a/dovecot-core.lintian-overrides b/dovecot-core.lintian-overrides new file mode 100644 index 0000000..f0c74da --- /dev/null +++ b/dovecot-core.lintian-overrides @@ -0,0 +1,59 @@ +# hardening flags are set and blhc succeeds; might need some investigation +dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/config] +dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/gdbhelper] +dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/libdovecot-storage.so.0.0.0] +dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/modules/lib20_fts_plugin.so] +dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/script-login] +dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/script] +dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/xml2text] +dovecot-core: package-contains-empty-directory [usr/share/dovecot/protocols.d/] +dovecot-core: package-contains-empty-directory [usr/lib/dovecot/modules/dict/] +dovecot-core: spelling-error-in-readme-debian dovenull dovenull (duplicate word) dovenull [usr/share/doc/dovecot-core/README.Debian] +dovecot-core: library-not-linked-against-libc [usr/lib/dovecot/modules/old-stats/libold_stats_mail.so] +dovecot-core: library-not-linked-against-libc [usr/lib/dovecot/modules/old-stats/libstats_auth.so] +dovecot-core [armel]: library-not-linked-against-libc usr/lib/dovecot/modules/lib20_listescape_plugin.so +# ignore internal libraries without dependency information +dovecot-core: shared-library-lacks-prerequisites [usr/lib/dovecot/modules/lib20_listescape_plugin.so] +dovecot-core: shared-library-lacks-prerequisites [usr/lib/dovecot/modules/settings/libpigeonhole_settings.so] +# ignore extra man pages +dovecot-core: spare-manual-page [usr/share/man/man1/deliver.1.gz] +# keep all man pages in section 1 for now +dovecot-core: manual-page-for-system-command [usr/sbin/dovecot] +# ignore executable-in-usr-lib, maybe move to libexec at some point +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/anvil] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/auth] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/config] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/dict] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/dns-client] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/doveadm-server] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/dovecot-lda] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/gdbhelper] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/health-check.sh] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/indexer] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/indexer-worker] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/log] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/quota-status] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/rawlog] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/script] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/script-login] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/stats] +dovecot-core: executable-in-usr-lib [usr/lib/dovecot/xml2text] +# ignore equal stub files +dovecot-core: duplicate-files usr/share/doc/dovecot-core/wiki/Pigeonhole.ManageSieve.Install.txt usr/share/doc/dovecot-core/wiki/Plugins.Stats.txt +# ignore stopwords txt files not in /usr/share/doc +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_da.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_de.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_en.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_es.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_fi.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_fr.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_it.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_nl.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_no.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_pt.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_ro.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_ru.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_sv.txt] +dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_tr.txt] +# ulimit -c might be specified but is supported by bash and dash +dovecot-core: bash-term-in-posix-shell '| ulimit' [etc/init.d/dovecot:43] diff --git a/dovecot-core.manpages b/dovecot-core.manpages new file mode 100644 index 0000000..926f5cc --- /dev/null +++ b/dovecot-core.manpages @@ -0,0 +1 @@ +debian/maildirmake.dovecot.1 diff --git a/dovecot-core.postinst b/dovecot-core.postinst new file mode 100644 index 0000000..1ce4d49 --- /dev/null +++ b/dovecot-core.postinst @@ -0,0 +1,95 @@ +#!/bin/sh +set -e + +if [ "$1" = "configure" ]; then + CONFFILES="dovecot.conf \ + conf.d/10-auth.conf \ + conf.d/10-logging.conf \ + conf.d/10-mail.conf \ + conf.d/10-master.conf \ + conf.d/10-metrics.conf \ + conf.d/10-ssl.conf \ + conf.d/15-lda.conf \ + conf.d/15-mailboxes.conf \ + conf.d/30-dict-server.conf \ + conf.d/90-acl.conf \ + conf.d/90-fts.conf \ + conf.d/90-quota.conf \ + conf.d/auth-deny.conf.ext \ + conf.d/auth-master.conf.ext \ + conf.d/auth-oauth2.conf.ext \ + conf.d/auth-passwdfile.conf.ext \ + conf.d/auth-sql.conf.ext \ + conf.d/auth-static.conf.ext \ + conf.d/auth-system.conf.ext" + + OLD_CONFFILES="conf.d/auth-vpopmail.conf.ext \ + dovecot-dict-auth.conf.ext \ + dovecot-dict-sql.conf.ext \ + dovecot-sql.conf.ext \ + conf.d/10-director.conf \ + conf.d/10-tcpwrapper.conf \ + conf.d/90-plugin.conf" + + ## Users + # + + adduser --system --group --home /usr/lib/dovecot --gecos "Dovecot mail server" \ + --no-create-home --disabled-password --quiet dovecot || true + + adduser --system --group --home /nonexistent --no-create-home --gecos "Dovecot login user" \ + --disabled-password --quiet dovenull || true + + for conffile in $CONFFILES ; do + # Tell ucf that the file in /usr/share/dovecot is the latest + # maintainer version, and let it handle how to manage the real + # configuration file in /etc/dovecot. + ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile" + ucfr dovecot-core "/etc/dovecot/$conffile" + if [ "$conffile" != "dovecot.conf" ] && [ -f "/etc/dovecot/$conffile" ] && + [ "$(echo "$conffile" | cut -b -7)" != "conf.d/" ]; then + chmod 0640 "/etc/dovecot/$conffile" + chgrp dovecot "/etc/dovecot/$conffile" + fi + done + + for oldconffile in $OLD_CONFFILES ; do + if [ -e "/etc/dovecot/$oldconffile" ]; then + modified=$(ucfq -w /etc/dovecot/$oldconffile | cut -d: -f4) + if [ "$modified" = "No" ]; then + echo "Removing unmodified obsolete configuration file '/etc/dovecot/$oldconffile'" + rm -f "/etc/dovecot/$oldconffile" + else + echo "Locally modified configuration file '/etc/dovecot/$oldconffile' is obsolete. Please remove." + fi + ucf --purge "/etc/dovecot/$oldconffile" + ucfr --purge dovecot-core "/etc/dovecot/$oldconffile" + fi + done + + + if ! dpkg-statoverride --list /etc/dovecot/private >/dev/null; then + dpkg-statoverride --quiet --update --add root root 0700 /etc/dovecot/private + fi + + # SSL configuration + # Use the ssl-cert-snakeoil certificate in the following cases: + # - On new installations + if [ -z "$2" ]; then + if [ ! -e /etc/dovecot/private/dovecot.key ] && \ + [ ! -e /etc/dovecot/private/dovecot.pem ] && \ + [ -e /etc/ssl/certs/ssl-cert-snakeoil.pem ] && \ + [ -e /etc/ssl/private/ssl-cert-snakeoil.key ]; then + ln -s /etc/ssl/certs/ssl-cert-snakeoil.pem /etc/dovecot/private/dovecot.pem + ln -s /etc/ssl/private/ssl-cert-snakeoil.key /etc/dovecot/private/dovecot.key + fi + fi +fi + +if [ "$1" = "triggered" ]; then + if [ -x "/etc/init.d/dovecot" ]; then + invoke-rc.d dovecot restart + fi +fi + +#DEBHELPER# diff --git a/dovecot-core.postrm b/dovecot-core.postrm new file mode 100644 index 0000000..87f6d12 --- /dev/null +++ b/dovecot-core.postrm @@ -0,0 +1,48 @@ +#!/bin/sh +set -e + +if [ "$1" = "purge" ] ; then + for conffile in $(ucfq --with-colons dovecot-core | cut -d: -f1); do + # we mimic dpkg as closely as possible, so we remove configuration + # files with dpkg backup extensions too: + ### Some of the following is from Tore Anderson: + for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist; do + rm -f "$conffile$ext" + done + # remove the configuration file itself + rm -f "$conffile" + # and finally clear it out from the ucf database + if which ucf >/dev/null; then + ucf --purge "$conffile" + fi + if which ucfr >/dev/null; then + ucfr --purge dovecot-core "$conffile" + fi + done + + userdel dovecot || true; + userdel dovenull || true; + + if [ -d /run/dovecot ]; then + rm -rf /run/dovecot + fi + + # Remove dovecot.pem and dovecot.key only if they are symlinks; otherwise + # we might remove CA-issued certificates that are difficult and/or + # expensive to replace. + for file in /etc/dovecot/private/dovecot.pem /etc/dovecot/private/dovecot.key; do + if [ -h "$file" ]; then + rm -f "$file" + else + echo "${file} is not a symbolic link, skipping removal." >&2 + fi + done +fi + +if [ "$1" = "triggered" ]; then + if [ -x "/etc/init.d/dovecot" ]; then + invoke-rc.d dovecot start + fi +fi + +#DEBHELPER# diff --git a/dovecot-core.triggers b/dovecot-core.triggers new file mode 100644 index 0000000..9f8f2ff --- /dev/null +++ b/dovecot-core.triggers @@ -0,0 +1,2 @@ +interest-noawait register-dovecot-plugin +interest-noawait /usr/lib/dovecot/modules diff --git a/dovecot-dev.README.Debian b/dovecot-dev.README.Debian new file mode 100644 index 0000000..cdcf38f --- /dev/null +++ b/dovecot-dev.README.Debian @@ -0,0 +1,10 @@ +/usr/share/dovecot/dovecot-abi contains that portion of DOVECOT_ABI_VERSION +from configure.ac outside parentheses (i.e not including the package version.) +It might look something like this: + +2.2.ABIv10 + +...packages providing plugins for dovecot can use this information in their +dependencies to avoid having to update every time there is a minor update to +the dovecot package. + diff --git a/dovecot-dev.lintian-overrides b/dovecot-dev.lintian-overrides new file mode 100644 index 0000000..1d459c2 --- /dev/null +++ b/dovecot-dev.lintian-overrides @@ -0,0 +1,2 @@ +# plugins probably need the package name +dovecot-dev: package-name-defined-in-config-h usr/include/dovecot/config.h diff --git a/dovecot-flatcurve.links b/dovecot-flatcurve.links new file mode 100644 index 0000000..6d5bbee --- /dev/null +++ b/dovecot-flatcurve.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-flatcurve diff --git a/dovecot-flatcurve.postinst b/dovecot-flatcurve.postinst new file mode 100644 index 0000000..1a1a324 --- /dev/null +++ b/dovecot-flatcurve.postinst @@ -0,0 +1,21 @@ +#!/bin/sh + +set -e + +if [ "$1" = "configure" ]; then + CONFFILES="conf.d/90-fts-flatcurve.conf" + + for conffile in $CONFFILES ; do + # Tell ucf that the file in /usr/share/dovecot is the latest + # maintainer version, and let it handle how to manage the real + # configuration file in /etc/dovecot. + ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile" + ucfr dovecot-ldap "/etc/dovecot/$conffile" + if [ "$conffile" != "dovecot.conf" ] && [ -f "/etc/dovecot/$conffile" ] && + [ "$(echo "$conffile" | cut -b -7)" != "conf.d/" ]; then + chmod 0600 "/etc/dovecot/$conffile" + fi + done +fi + +#DEBHELPER# diff --git a/dovecot-flatcurve.postrm b/dovecot-flatcurve.postrm new file mode 100644 index 0000000..c0cddcd --- /dev/null +++ b/dovecot-flatcurve.postrm @@ -0,0 +1,25 @@ +#!/bin/sh +set -e + +if [ "$1" = "purge" ] ; then + for conffile in $(ucfq --with-colons dovecot-flatcurve | cut -d: -f1); do + # we mimic dpkg as closely as possible, so we remove configuration + # files with dpkg backup extensions too: + ### Some of the following is from Tore Anderson: + for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist; do + rm -f "$conffile$ext" + done + # remove the configuration file itself + rm -f "$conffile" + # and finally clear it out from the ucf database + if which ucf >/dev/null; then + ucf --purge "$conffile" + fi + if which ucfr >/dev/null; then + ucfr --purge dovecot-flatcurve "$conffile" + fi + done + +fi + +#DEBHELPER# diff --git a/dovecot-flatcurve.prerm b/dovecot-flatcurve.prerm new file mode 100644 index 0000000..2ad0a1f --- /dev/null +++ b/dovecot-flatcurve.prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +if [ -d /run/systemd/system ]; then + deb-systemd-invoke stop dovecot.socket || true +fi + +if [ -x "/etc/init.d/dovecot" ]; then + invoke-rc.d dovecot stop +fi + +#DEBHELPER# diff --git a/dovecot-flatcurve.triggers b/dovecot-flatcurve.triggers new file mode 100644 index 0000000..903b9e7 --- /dev/null +++ b/dovecot-flatcurve.triggers @@ -0,0 +1,2 @@ +activate-noawait register-dovecot-plugin + diff --git a/dovecot-gssapi.links b/dovecot-gssapi.links new file mode 100644 index 0000000..dde94f9 --- /dev/null +++ b/dovecot-gssapi.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-gssapi diff --git a/dovecot-imapd.links b/dovecot-imapd.links new file mode 100644 index 0000000..d0fb029 --- /dev/null +++ b/dovecot-imapd.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-imapd diff --git a/dovecot-imapd.lintian-overrides b/dovecot-imapd.lintian-overrides new file mode 100644 index 0000000..dc26e0d --- /dev/null +++ b/dovecot-imapd.lintian-overrides @@ -0,0 +1,2 @@ +# hardening flags are set and blhc succeeds; might need some investigation +dovecot-imapd: hardening-no-fortify-functions [usr/lib/dovecot/imap] diff --git a/dovecot-imapd.postinst b/dovecot-imapd.postinst new file mode 100644 index 0000000..1b134b6 --- /dev/null +++ b/dovecot-imapd.postinst @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +if [ "$1" = "configure" ]; then + CONFFILES="conf.d/20-imap.conf" + + for conffile in $CONFFILES ; do + # Tell ucf that the file in /usr/share/dovecot is the latest + # maintainer version, and let it handle how to manage the real + # configuration file in /etc/dovecot. + ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile" + ucfr dovecot-imapd "/etc/dovecot/$conffile" + done + + cat < /usr/share/dovecot/protocols.d/imapd.protocol +protocols { + imap = yes +} +EOF +fi + +#DEBHELPER# diff --git a/dovecot-imapd.postrm b/dovecot-imapd.postrm new file mode 100644 index 0000000..dcaaeaf --- /dev/null +++ b/dovecot-imapd.postrm @@ -0,0 +1,29 @@ +#!/bin/sh +set -e + +if [ "$1" = "purge" ] ; then + for conffile in $(ucfq --with-colons dovecot-imapd | cut -d: -f1); do + # we mimic dpkg as closely as possible, so we remove configuration + # files with dpkg backup extensions too: + ### Some of the following is from Tore Anderson: + for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist; do + rm -f "$conffile$ext" + done + # remove the configuration file itself + rm -f "$conffile" + # and finally clear it out from the ucf database + if which ucf >/dev/null; then + ucf --purge "$conffile" + fi + if which ucfr >/dev/null; then + ucfr --purge dovecot-imapd "$conffile" + fi + done + +fi + +if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then + rm -f /usr/share/dovecot/protocols.d/imapd.protocol +fi + +#DEBHELPER# diff --git a/dovecot-imapd.prerm b/dovecot-imapd.prerm new file mode 100644 index 0000000..2ad0a1f --- /dev/null +++ b/dovecot-imapd.prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +if [ -d /run/systemd/system ]; then + deb-systemd-invoke stop dovecot.socket || true +fi + +if [ -x "/etc/init.d/dovecot" ]; then + invoke-rc.d dovecot stop +fi + +#DEBHELPER# diff --git a/dovecot-imapd.triggers b/dovecot-imapd.triggers new file mode 100644 index 0000000..a71d0af --- /dev/null +++ b/dovecot-imapd.triggers @@ -0,0 +1 @@ +activate-noawait register-dovecot-plugin diff --git a/dovecot-imapd.ufw.profile b/dovecot-imapd.ufw.profile new file mode 100644 index 0000000..17db05c --- /dev/null +++ b/dovecot-imapd.ufw.profile @@ -0,0 +1,11 @@ +[Dovecot IMAP] +title=Secure mail server (IMAP) +description=Dovecot is a mail server whose major goals are security and extreme + reliability. +ports=143/tcp + +[Dovecot Secure IMAP] +title=Secure mail server (IMAPS) +description=Dovecot is a mail server whose major goals are security and extreme + reliability. +ports=993/tcp diff --git a/dovecot-ldap.links b/dovecot-ldap.links new file mode 100644 index 0000000..0f3fcdb --- /dev/null +++ b/dovecot-ldap.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-ldap diff --git a/dovecot-ldap.postinst b/dovecot-ldap.postinst new file mode 100644 index 0000000..2a12fb9 --- /dev/null +++ b/dovecot-ldap.postinst @@ -0,0 +1,21 @@ +#!/bin/sh + +set -e + +if [ "$1" = "configure" ]; then + CONFFILES="conf.d/auth-ldap.conf.ext" + + for conffile in $CONFFILES ; do + # Tell ucf that the file in /usr/share/dovecot is the latest + # maintainer version, and let it handle how to manage the real + # configuration file in /etc/dovecot. + ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile" + ucfr dovecot-ldap "/etc/dovecot/$conffile" + if [ "$conffile" != "dovecot.conf" ] && [ -f "/etc/dovecot/$conffile" ] && + [ "$(echo "$conffile" | cut -b -7)" != "conf.d/" ]; then + chmod 0600 "/etc/dovecot/$conffile" + fi + done +fi + +#DEBHELPER# diff --git a/dovecot-ldap.postrm b/dovecot-ldap.postrm new file mode 100644 index 0000000..d0405ad --- /dev/null +++ b/dovecot-ldap.postrm @@ -0,0 +1,27 @@ +#!/bin/sh +set -e + +if [ "$1" = "purge" ] ; then + CONFFILES="/etc/dovecot/dovecot-ldap.conf.ext \ + /etc/dovecot/conf.d/auth-ldap.conf.ext" + + for conffile in $CONFFILES; do + # we mimic dpkg as closely as possible, so we remove configuration + # files with dpkg backup extensions too: + ### Some of the following is from Tore Anderson: + for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist; do + rm -f "$conffile$ext" + done + # remove the configuration file itself + rm -f "$conffile" + # and finally clear it out from the ucf database + if which ucf >/dev/null; then + ucf --purge "$conffile" + fi + if which ucfr >/dev/null; then + ucfr --purge dovecot-ldap "$conffile" + fi + done +fi + +#DEBHELPER# diff --git a/dovecot-ldap.triggers b/dovecot-ldap.triggers new file mode 100644 index 0000000..903b9e7 --- /dev/null +++ b/dovecot-ldap.triggers @@ -0,0 +1,2 @@ +activate-noawait register-dovecot-plugin + diff --git a/dovecot-lmtpd.links b/dovecot-lmtpd.links new file mode 100644 index 0000000..bc95bf0 --- /dev/null +++ b/dovecot-lmtpd.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-lmtpd diff --git a/dovecot-lmtpd.postinst b/dovecot-lmtpd.postinst new file mode 100644 index 0000000..de561e1 --- /dev/null +++ b/dovecot-lmtpd.postinst @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +if [ "$1" = "configure" ]; then + CONFFILES="conf.d/20-lmtp.conf" + + for conffile in $CONFFILES ; do + # Tell ucf that the file in /usr/share/dovecot is the latest + # maintainer version, and let it handle how to manage the real + # configuration file in /etc/dovecot. + ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile" + ucfr dovecot-lmtpd "/etc/dovecot/$conffile" + done + + cat < /usr/share/dovecot/protocols.d/lmtpd.protocol +protocols { + lmtp = yes +} +EOF +fi + +#DEBHELPER# diff --git a/dovecot-lmtpd.postrm b/dovecot-lmtpd.postrm new file mode 100644 index 0000000..f0b9920 --- /dev/null +++ b/dovecot-lmtpd.postrm @@ -0,0 +1,29 @@ +#!/bin/sh +set -e + +if [ "$1" = "purge" ] ; then + for conffile in $(ucfq --with-colons dovecot-lmtpd | cut -d: -f1); do + # we mimic dpkg as closely as possible, so we remove configuration + # files with dpkg backup extensions too: + ### Some of the following is from Tore Anderson: + for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist; do + rm -f "$conffile$ext" + done + # remove the configuration file itself + rm -f "$conffile" + # and finally clear it out from the ucf database + if which ucf >/dev/null; then + ucf --purge "$conffile" + fi + if which ucfr >/dev/null; then + ucfr --purge dovecot-lmtpd "$conffile" + fi + done + +fi + +if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then + rm -f /usr/share/dovecot/protocols.d/lmtpd.protocol +fi + +#DEBHELPER# diff --git a/dovecot-lmtpd.prerm b/dovecot-lmtpd.prerm new file mode 100644 index 0000000..2ad0a1f --- /dev/null +++ b/dovecot-lmtpd.prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +if [ -d /run/systemd/system ]; then + deb-systemd-invoke stop dovecot.socket || true +fi + +if [ -x "/etc/init.d/dovecot" ]; then + invoke-rc.d dovecot stop +fi + +#DEBHELPER# diff --git a/dovecot-lmtpd.triggers b/dovecot-lmtpd.triggers new file mode 100644 index 0000000..903b9e7 --- /dev/null +++ b/dovecot-lmtpd.triggers @@ -0,0 +1,2 @@ +activate-noawait register-dovecot-plugin + diff --git a/dovecot-managesieved.README.Debian b/dovecot-managesieved.README.Debian new file mode 100644 index 0000000..6c6aa4a --- /dev/null +++ b/dovecot-managesieved.README.Debian @@ -0,0 +1,17 @@ +Removing dovecot-managesieved +----------------------------- + +If you remove (i.e. with dpkg -r or aptitude remove) this package you will +get an error something like this: + +doveconf: Fatal: Error in configuration file /etc/dovecot/dovecot.conf: +service(managesieve-login): executable is empty + +To avoid it, make sure +/etc/dovecot/conf.d/etc/dovecot/conf.d/20-managesieve.conf has been deleted, +moved or completely commented out first. Alternatively, purge the package +instead of removing it. + +If you do not do this, dovecot-core will be left in an unconfigured state +because the error will cause a trigger to fail. To fix this simply reinstall +dovecot-core. diff --git a/dovecot-managesieved.install b/dovecot-managesieved.install new file mode 100644 index 0000000..14e7229 --- /dev/null +++ b/dovecot-managesieved.install @@ -0,0 +1 @@ +pigeonhole/doc/example-config/conf.d/??-managesieve* usr/share/dovecot/conf.d diff --git a/dovecot-managesieved.links b/dovecot-managesieved.links new file mode 100644 index 0000000..9f3ea79 --- /dev/null +++ b/dovecot-managesieved.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-managesieved diff --git a/dovecot-managesieved.lintian-overrides b/dovecot-managesieved.lintian-overrides new file mode 100644 index 0000000..4d9c9a1 --- /dev/null +++ b/dovecot-managesieved.lintian-overrides @@ -0,0 +1,2 @@ +dovecot-managesieved: shared-library-lacks-prerequisites [usr/lib/dovecot/modules/settings/libmanagesieve_settings.so] +dovecot-managesieved: library-not-linked-against-libc [usr/lib/dovecot/modules/settings/libmanagesieve_settings.so] diff --git a/dovecot-managesieved.postinst b/dovecot-managesieved.postinst new file mode 100644 index 0000000..4544e1e --- /dev/null +++ b/dovecot-managesieved.postinst @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +if [ "$1" = "configure" ]; then + CONFFILES="conf.d/20-managesieve.conf" + + for conffile in $CONFFILES ; do + # Tell ucf that the file in /usr/share/dovecot is the latest + # maintainer version, and let it handle how to manage the real + # configuration file in /etc/dovecot. + ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile" + ucfr dovecot-managesieved "/etc/dovecot/$conffile" + done + + cat < /usr/share/dovecot/protocols.d/managesieved.protocol +protocols { + sieve = yes +} +EOF +fi + +#DEBHELPER# diff --git a/dovecot-managesieved.postrm b/dovecot-managesieved.postrm new file mode 100644 index 0000000..f464ccf --- /dev/null +++ b/dovecot-managesieved.postrm @@ -0,0 +1,29 @@ +#!/bin/sh +set -e + +if [ "$1" = "purge" ] ; then + for conffile in $(ucfq --with-colons dovecot-managesieved | cut -d: -f1); do + # we mimic dpkg as closely as possible, so we remove configuration + # files with dpkg backup extensions too: + ### Some of the following is from Tore Anderson: + for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist; do + rm -f "$conffile$ext" + done + # remove the configuration file itself + rm -f "$conffile" + # and finally clear it out from the ucf database + if which ucf >/dev/null; then + ucf --purge "$conffile" + fi + if which ucfr >/dev/null; then + ucfr --purge dovecot-managesieved "$conffile" + fi + done + +fi + +if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then + rm -f /usr/share/dovecot/protocols.d/managesieved.protocol +fi + +#DEBHELPER# diff --git a/dovecot-managesieved.prerm b/dovecot-managesieved.prerm new file mode 100644 index 0000000..2ad0a1f --- /dev/null +++ b/dovecot-managesieved.prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +if [ -d /run/systemd/system ]; then + deb-systemd-invoke stop dovecot.socket || true +fi + +if [ -x "/etc/init.d/dovecot" ]; then + invoke-rc.d dovecot stop +fi + +#DEBHELPER# diff --git a/dovecot-managesieved.triggers b/dovecot-managesieved.triggers new file mode 100644 index 0000000..903b9e7 --- /dev/null +++ b/dovecot-managesieved.triggers @@ -0,0 +1,2 @@ +activate-noawait register-dovecot-plugin + diff --git a/dovecot-mysql.links b/dovecot-mysql.links new file mode 100644 index 0000000..321547d --- /dev/null +++ b/dovecot-mysql.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-mysql diff --git a/dovecot-mysql.triggers b/dovecot-mysql.triggers new file mode 100644 index 0000000..903b9e7 --- /dev/null +++ b/dovecot-mysql.triggers @@ -0,0 +1,2 @@ +activate-noawait register-dovecot-plugin + diff --git a/dovecot-openssl.cnf b/dovecot-openssl.cnf new file mode 100644 index 0000000..90bf50c --- /dev/null +++ b/dovecot-openssl.cnf @@ -0,0 +1,23 @@ +# +# SSLeay configuration file for Dovecot. +# + +RANDFILE = /dev/urandom + +[ req ] +default_bits = 2048 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +prompt = no +policy = policy_anything +req_extensions = v3_req +x509_extensions = v3_req + +[ req_distinguished_name ] +organizationName = Dovecot mail server +organizationalUnitName = @commonName@ +commonName = @commonName@ +emailAddress = @emailAddress@ + +[ v3_req ] +basicConstraints = CA:FALSE diff --git a/dovecot-pgsql.links b/dovecot-pgsql.links new file mode 100644 index 0000000..9b4831f --- /dev/null +++ b/dovecot-pgsql.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-pgsql diff --git a/dovecot-pgsql.triggers b/dovecot-pgsql.triggers new file mode 100644 index 0000000..903b9e7 --- /dev/null +++ b/dovecot-pgsql.triggers @@ -0,0 +1,2 @@ +activate-noawait register-dovecot-plugin + diff --git a/dovecot-pop3d.links b/dovecot-pop3d.links new file mode 100644 index 0000000..24ce7f6 --- /dev/null +++ b/dovecot-pop3d.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-pop3d diff --git a/dovecot-pop3d.postinst b/dovecot-pop3d.postinst new file mode 100644 index 0000000..8560956 --- /dev/null +++ b/dovecot-pop3d.postinst @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +if [ "$1" = "configure" ]; then + CONFFILES="conf.d/20-pop3.conf" + + for conffile in $CONFFILES ; do + # Tell ucf that the file in /usr/share/dovecot is the latest + # maintainer version, and let it handle how to manage the real + # configuration file in /etc/dovecot. + ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile" + ucfr dovecot-pop3d "/etc/dovecot/$conffile" + done + + cat < /usr/share/dovecot/protocols.d/pop3d.protocol +protocols { + pop3 = yes +} +EOF +fi + +#DEBHELPER# diff --git a/dovecot-pop3d.postrm b/dovecot-pop3d.postrm new file mode 100644 index 0000000..0c858f1 --- /dev/null +++ b/dovecot-pop3d.postrm @@ -0,0 +1,29 @@ +#!/bin/sh +set -e + +if [ "$1" = "purge" ] ; then + for conffile in $(ucfq --with-colons dovecot-pop3d | cut -d: -f1); do + # we mimic dpkg as closely as possible, so we remove configuration + # files with dpkg backup extensions too: + ### Some of the following is from Tore Anderson: + for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist; do + rm -f "$conffile$ext" + done + # remove the configuration file itself + rm -f "$conffile" + # and finally clear it out from the ucf database + if which ucf >/dev/null; then + ucf --purge "$conffile" + fi + if which ucfr >/dev/null; then + ucfr --purge dovecot-pop3d "$conffile" + fi + done + +fi + +if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then + rm -f /usr/share/dovecot/protocols.d/pop3d.protocol +fi + +#DEBHELPER# diff --git a/dovecot-pop3d.prerm b/dovecot-pop3d.prerm new file mode 100644 index 0000000..2ad0a1f --- /dev/null +++ b/dovecot-pop3d.prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +if [ -d /run/systemd/system ]; then + deb-systemd-invoke stop dovecot.socket || true +fi + +if [ -x "/etc/init.d/dovecot" ]; then + invoke-rc.d dovecot stop +fi + +#DEBHELPER# diff --git a/dovecot-pop3d.triggers b/dovecot-pop3d.triggers new file mode 100644 index 0000000..903b9e7 --- /dev/null +++ b/dovecot-pop3d.triggers @@ -0,0 +1,2 @@ +activate-noawait register-dovecot-plugin + diff --git a/dovecot-pop3d.ufw.profile b/dovecot-pop3d.ufw.profile new file mode 100644 index 0000000..a46df40 --- /dev/null +++ b/dovecot-pop3d.ufw.profile @@ -0,0 +1,11 @@ +[Dovecot POP3] +title=Secure mail server (POP3) +description=Dovecot is a mail server whose major goals are security and extreme + reliability. +ports=110/tcp + +[Dovecot Secure POP3] +title=Secure mail server (POP3S) +description=Dovecot is a mail server whose major goals are security and extreme + reliability. +ports=995/tcp diff --git a/dovecot-sieve.links b/dovecot-sieve.links new file mode 100644 index 0000000..971b50a --- /dev/null +++ b/dovecot-sieve.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-sieve diff --git a/dovecot-sieve.lintian-overrides b/dovecot-sieve.lintian-overrides new file mode 100644 index 0000000..840cfef --- /dev/null +++ b/dovecot-sieve.lintian-overrides @@ -0,0 +1,4 @@ +# ignore internal library without dependency information +dovecot-sieve: shared-library-lacks-prerequisites [usr/lib/dovecot/modules/sieve/lib90_sieve_imapsieve_plugin.so] +# alias for sieve-dump +dovecot-sieve: spare-manual-page [usr/share/man/man1/sieved.1.gz] diff --git a/dovecot-sieve.postinst b/dovecot-sieve.postinst new file mode 100644 index 0000000..6fb5244 --- /dev/null +++ b/dovecot-sieve.postinst @@ -0,0 +1,18 @@ +#!/bin/sh + +set -e + +if [ "$1" = "configure" ]; then + CONFFILES="conf.d/90-sieve.conf \ + conf.d/90-sieve-extprograms.conf" + + for conffile in $CONFFILES ; do + # Tell ucf that the file in /usr/share/dovecot is the latest + # maintainer version, and let it handle how to manage the real + # configuration file in /etc/dovecot. + ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile" + ucfr dovecot-sieve "/etc/dovecot/$conffile" + done +fi + +#DEBHELPER# diff --git a/dovecot-sieve.postrm b/dovecot-sieve.postrm new file mode 100644 index 0000000..db48cfc --- /dev/null +++ b/dovecot-sieve.postrm @@ -0,0 +1,24 @@ +#!/bin/sh +set -e + +if [ "$1" = "purge" ] ; then + for conffile in $(ucfq --with-colons dovecot-sieve | cut -d: -f1); do + # we mimic dpkg as closely as possible, so we remove configuration + # files with dpkg backup extensions too: + ### Some of the following is from Tore Anderson: + for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist; do + rm -f "$conffile$ext" + done + # remove the configuration file itself + rm -f "$conffile" + # and finally clear it out from the ucf database + if which ucf >/dev/null; then + ucf --purge "$conffile" + fi + if which ucfr >/dev/null; then + ucfr --purge dovecot-sieve "$conffile" + fi + done +fi + +#DEBHELPER# diff --git a/dovecot-sieve.triggers b/dovecot-sieve.triggers new file mode 100644 index 0000000..903b9e7 --- /dev/null +++ b/dovecot-sieve.triggers @@ -0,0 +1,2 @@ +activate-noawait register-dovecot-plugin + diff --git a/dovecot-solr.README.Debian b/dovecot-solr.README.Debian new file mode 100644 index 0000000..edc5d75 --- /dev/null +++ b/dovecot-solr.README.Debian @@ -0,0 +1,2 @@ +After installing this package, the solr XML schema can be found in +/usr/share/dovecot/solr-schema-*.xml diff --git a/dovecot-solr.install b/dovecot-solr.install new file mode 100644 index 0000000..c14caf4 --- /dev/null +++ b/dovecot-solr.install @@ -0,0 +1 @@ +doc/solr-*.xml usr/share/dovecot diff --git a/dovecot-solr.links b/dovecot-solr.links new file mode 100644 index 0000000..7536822 --- /dev/null +++ b/dovecot-solr.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-solr diff --git a/dovecot-solr.triggers b/dovecot-solr.triggers new file mode 100644 index 0000000..903b9e7 --- /dev/null +++ b/dovecot-solr.triggers @@ -0,0 +1,2 @@ +activate-noawait register-dovecot-plugin + diff --git a/dovecot-sqlite.links b/dovecot-sqlite.links new file mode 100644 index 0000000..2204ea7 --- /dev/null +++ b/dovecot-sqlite.links @@ -0,0 +1 @@ +/usr/share/bug/dovecot-core /usr/share/bug/dovecot-sqlite diff --git a/dovecot-sqlite.triggers b/dovecot-sqlite.triggers new file mode 100644 index 0000000..903b9e7 --- /dev/null +++ b/dovecot-sqlite.triggers @@ -0,0 +1,2 @@ +activate-noawait register-dovecot-plugin + diff --git a/dovecot-submissiond.postinst b/dovecot-submissiond.postinst new file mode 100644 index 0000000..7459606 --- /dev/null +++ b/dovecot-submissiond.postinst @@ -0,0 +1,27 @@ +#!/bin/sh + +set -e + +if [ "$1" = "configure" ]; then + CONFFILES="conf.d/20-submission.conf" + + for conffile in $CONFFILES ; do + # Tell ucf that the file in /usr/share/dovecot is the latest + # maintainer version, and let it handle how to manage the real + # configuration file in /etc/dovecot. + ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile" + # Use --force to allow hijacking the file from dovecot-submission + ucfr --force dovecot-submissiond "/etc/dovecot/$conffile" + done + + if [ -f /usr/share/dovecot/protocols.d/submission.protocol ]; then + rm -f /usr/share/dovecot/protocols.d/submission.protocol + fi + cat < /usr/share/dovecot/protocols.d/submissiond.protocol +protocols { + submission = yes +} +EOF +fi + +#DEBHELPER# diff --git a/dovecot-submissiond.postrm b/dovecot-submissiond.postrm new file mode 100644 index 0000000..eaea028 --- /dev/null +++ b/dovecot-submissiond.postrm @@ -0,0 +1,29 @@ +#!/bin/sh +set -e + +if [ "$1" = "purge" ] ; then + for conffile in $(ucfq --with-colons dovecot-submissiond | cut -d: -f1); do + # we mimic dpkg as closely as possible, so we remove configuration + # files with dpkg backup extensions too: + ### Some of the following is from Tore Anderson: + for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist; do + rm -f "$conffile$ext" + done + # remove the configuration file itself + rm -f "$conffile" + # and finally clear it out from the ucf database + if which ucf >/dev/null; then + ucf --purge "$conffile" + fi + if which ucfr >/dev/null; then + ucfr --purge dovecot-submissiond "$conffile" + fi + done + +fi + +if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then + rm -f /usr/share/dovecot/protocols.d/submissiond.protocol +fi + +#DEBHELPER# diff --git a/dovecot-submissiond.prerm b/dovecot-submissiond.prerm new file mode 100644 index 0000000..2ad0a1f --- /dev/null +++ b/dovecot-submissiond.prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +if [ -d /run/systemd/system ]; then + deb-systemd-invoke stop dovecot.socket || true +fi + +if [ -x "/etc/init.d/dovecot" ]; then + invoke-rc.d dovecot stop +fi + +#DEBHELPER# diff --git a/dovecot-submissiond.triggers b/dovecot-submissiond.triggers new file mode 100644 index 0000000..a71d0af --- /dev/null +++ b/dovecot-submissiond.triggers @@ -0,0 +1 @@ +activate-noawait register-dovecot-plugin diff --git a/gbp.conf b/gbp.conf new file mode 100644 index 0000000..d2026bc --- /dev/null +++ b/gbp.conf @@ -0,0 +1,14 @@ +[DEFAULT] +pristine-tar = True +sign-tags = True +component=pigeonhole + +[buildpackage] +export-dir = ../build-area/ + +[dch] +meta = True +id-length = 7 + +[pq] +patch-numbers = False diff --git a/maildirmake.dovecot b/maildirmake.dovecot new file mode 100644 index 0000000..5313d13 --- /dev/null +++ b/maildirmake.dovecot @@ -0,0 +1,28 @@ +#!/bin/sh +# +# maildirmake.dovecot -- create maildirs +# Copyright (c) 2003, Jaldhar H. Vyas +# "Do what thou wilt" shall be the whole of the license. +# +dir="$1" +owner="$2" +if [ -z "$dir" ]; then + echo "Must supply a directory path" + exit 1 +fi + +if [ "$dir" = "-h" ]; then + echo "usage: $0 directory [user]" + exit 0 +fi + +umask 077 +mkdir -p "$dir/cur" "$dir/new" "$dir/tmp" || exit 1 +chmod 0700 "$dir" "$dir/cur" "$dir/new" "$dir/tmp" || exit 1 + +if [ -n "$owner" ]; then + chown -R "$owner" "$dir" || exit 1 +fi + +exit 0 + diff --git a/maildirmake.dovecot.1 b/maildirmake.dovecot.1 new file mode 100644 index 0000000..a814c81 --- /dev/null +++ b/maildirmake.dovecot.1 @@ -0,0 +1,46 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" First parameter, NAME, should be all caps +.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection +.\" other parameters are allowed: see man(7), man(1) +.TH "MAILDIRMAKE.DOVECOT" "1" "23 November 2005" +.\" Please adjust this date whenever revising the manpage. +.\" +.\" Some roff macros, for reference: +.\" .nh disable hyphenation +.\" .hy enable hyphenation +.\" .ad l left justify +.\" .ad b justify to both left and right margins +.\" .nf disable filling +.\" .fi enable filling +.\" .br insert line break +.\" .sp insert n+1 empty lines +.\" for manpage-specific macros, see man(7) +.SH NAME +maildirmake.dovecot \- creates maildirs and maildir subfolders +.SH SYNOPSIS +.B maildirmake.dovecot +\fBdirectory\fR [ \fRowner\fR ] \fR +.br +.SH DESCRIPTION +.\" TeX users may be more comfortable with the \fB\fP and +.\" \fI\fP escape sequences to invode bold face and italics, +.\" respectively. +\fBmaildirmake.dovecot\fP creates a maildir and its maildir subfolders (cur, new, tmp). +\fBdirectory\fP is the name of the new maildir. If \fBdirectory\fP exists cur, new, tmp directories are created inside it. +.TP +You can specify \fBowner\fP to change directories ownership if you have ownership modification permission. +.TP +\fBmaildirmake.dovecot\fP is very basic. It is strongly recommended to use \fBmaildirmake\fP if you want to do serious maildir management. +.SH OPTIONS +.TP +\fB-h\fP +Show options summary and exits immediately. +.SH SEE ALSO +.BR maildirmake (1). +.BR maildir (5). +.SH AUTHOR +.TP +dovecot was written by Timo Sirainen . maildirmake.dovecot script was written by Jaldhar H. Vyas +.PP +This manual page was written by Henry Precheur , +for the Debian project (but may be used by others). diff --git a/patches/0001-auth-db-oauth2-Reduce-nesting-in-token_in_scope-via-.patch b/patches/0001-auth-db-oauth2-Reduce-nesting-in-token_in_scope-via-.patch new file mode 100644 index 0000000..557c2fe --- /dev/null +++ b/patches/0001-auth-db-oauth2-Reduce-nesting-in-token_in_scope-via-.patch @@ -0,0 +1,71 @@ +From 4c8919f9470ee61efe318fa4f7d410ba12098eed Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 3 Jun 2026 12:56:13 +0000 +Subject: [PATCH 1/4] auth: db-oauth2: Reduce nesting in token_in_scope via + early return + +--- + src/auth/db-oauth2.c | 48 ++++++++++++++++++++++---------------------- + 1 file changed, 24 insertions(+), 24 deletions(-) + +Index: trixie/src/auth/db-oauth2.c +=================================================================== +--- trixie.orig/src/auth/db-oauth2.c ++++ trixie/src/auth/db-oauth2.c +@@ -553,32 +553,32 @@ static bool + db_oauth2_token_in_scope(struct db_oauth2_request *req, + enum passdb_result *result_r, const char **error_r) + { +- bool found = TRUE; +- if (!array_is_empty(&req->db->set->scope)) { +- found = FALSE; +- const char *value = auth_fields_find(req->fields, "scope"); +- bool has_scope = value != NULL; +- if (!has_scope) +- value = auth_fields_find(req->fields, "aud"); +- e_debug(authdb_event(req->auth_request), +- "Token scope(s): %s", +- value); +- if (value != NULL) { +- const char *wanted_scope; +- const char *const *entries = has_scope ? +- t_strsplit_spaces(value, " ") : +- t_strsplit_tabescaped(value); +- array_foreach_elem(&req->db->set->scope, wanted_scope) { +- if ((found = str_array_find(entries, wanted_scope))) +- break; +- } +- } +- if (!found) { +- *error_r = t_strdup_printf("Token is not valid for scope '%s'", +- req->db->oauth2_set.scope); +- *result_r = PASSDB_RESULT_USER_DISABLED; ++ if (array_is_empty(&req->db->set->scope)) ++ return TRUE; ++ ++ bool found = FALSE; ++ const char *value = auth_fields_find(req->fields, "scope"); ++ bool has_scope = value != NULL; ++ if (!has_scope) ++ value = auth_fields_find(req->fields, "aud"); ++ e_debug(authdb_event(req->auth_request), ++ "Token scope(s): %s", ++ value); ++ if (value != NULL) { ++ const char *wanted_scope; ++ const char *const *entries = has_scope ? ++ t_strsplit_spaces(value, " ") : ++ t_strsplit_tabescaped(value); ++ array_foreach_elem(&req->db->set->scope, wanted_scope) { ++ if ((found = str_array_find(entries, wanted_scope))) ++ break; + } + } ++ if (!found) { ++ *error_r = t_strdup_printf("Token is not valid for scope '%s'", ++ t_array_const_string_join(&req->db->set->scope, " ")); ++ *result_r = PASSDB_RESULT_USER_DISABLED; ++ } + return found; + } + diff --git a/patches/0001-commin-Rename-LOGIN_PROXY_FAILURE_TYPE_AUTH-to-LOGIN.patch b/patches/0001-commin-Rename-LOGIN_PROXY_FAILURE_TYPE_AUTH-to-LOGIN.patch new file mode 100644 index 0000000..05a13f5 --- /dev/null +++ b/patches/0001-commin-Rename-LOGIN_PROXY_FAILURE_TYPE_AUTH-to-LOGIN.patch @@ -0,0 +1,143 @@ +From d1b2e9e028b5bb8501b054c13c5208fbb4eff21f Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 18 Jul 2025 14:26:21 +0300 +Subject: [PATCH] *-commin: Rename LOGIN_PROXY_FAILURE_TYPE_AUTH to + LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED + +--- + src/imap-login/imap-proxy.c | 4 ++-- + src/login-common/client-common-auth.c | 2 +- + src/login-common/client-common.c | 2 +- + src/login-common/login-proxy.c | 4 ++-- + src/login-common/login-proxy.h | 2 +- + src/pop3-login/pop3-proxy.c | 4 ++-- + src/submission-login/submission-proxy.c | 4 ++-- + 7 files changed, 11 insertions(+), 11 deletions(-) + +Index: trixie/src/imap-login/imap-proxy.c +=================================================================== +--- trixie.orig/src/imap-login/imap-proxy.c ++++ trixie/src/imap-login/imap-proxy.c +@@ -427,7 +427,7 @@ int imap_proxy_parse_line(struct client + const char *log_line = line; + (void)str_begins_icase(log_line, "NO ", &log_line); + enum login_proxy_failure_type failure_type = +- LOGIN_PROXY_FAILURE_TYPE_AUTH; ++ LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED; + #define STR_NO_IMAP_RESP_CODE_AUTHFAILED "NO ["IMAP_RESP_CODE_AUTHFAILED"]" + if (str_begins_with(line, STR_NO_IMAP_RESP_CODE_AUTHFAILED)) { + /* the remote sent a generic "authentication failed" +@@ -586,7 +586,7 @@ imap_proxy_send_failure_reply(struct ima + client_send_raw(&imap_client->common, t_strconcat( + imap_client->cmd_tag, " NO ", reason, "\r\n", NULL)); + break; +- case LOGIN_PROXY_FAILURE_TYPE_AUTH: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: + /* reply was already sent */ + break; + } +Index: trixie/src/login-common/client-common-auth.c +=================================================================== +--- trixie.orig/src/login-common/client-common-auth.c ++++ trixie/src/login-common/client-common-auth.c +@@ -426,7 +426,7 @@ proxy_redirect_reauth_callback(struct au + /* Disconnect from the original backend */ + login_proxy_failed(client->login_proxy, + login_proxy_get_event(client->login_proxy), +- LOGIN_PROXY_FAILURE_TYPE_AUTH, ++ LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED, + t_strdup_printf("Redirected to %s", reply.proxy.host)); + return; + } +Index: trixie/src/login-common/client-common.c +=================================================================== +--- trixie.orig/src/login-common/client-common.c ++++ trixie/src/login-common/client-common.c +@@ -1385,7 +1385,7 @@ bool client_get_extra_disconnect_reason( + event_reason = "protocol_failure"; + last_reason = "protocol failure"; + break; +- case LOGIN_PROXY_FAILURE_TYPE_AUTH: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: + event_reason = "auth_failed"; + last_reason = "authentication failure"; + break; +Index: trixie/src/login-common/login-proxy.c +=================================================================== +--- trixie.orig/src/login-common/login-proxy.c ++++ trixie/src/login-common/login-proxy.c +@@ -829,7 +829,7 @@ bool login_proxy_failed(struct login_pro + case LOGIN_PROXY_FAILURE_TYPE_PROTOCOL: + log_prefix = "Remote server sent invalid input: "; + break; +- case LOGIN_PROXY_FAILURE_TYPE_AUTH: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: + log_prefix = ""; + try_reconnect = FALSE; + break; +@@ -854,7 +854,7 @@ bool login_proxy_failed(struct login_pro + return TRUE; + } + +- if (type != LOGIN_PROXY_FAILURE_TYPE_AUTH && ++ if (type != LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED && + type != LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL) + e_error(event, "%s%s", log_prefix, reason); + else if (proxy->client->set->auth_verbose) +Index: trixie/src/login-common/login-proxy.h +=================================================================== +--- trixie.orig/src/login-common/login-proxy.h ++++ trixie/src/login-common/login-proxy.h +@@ -34,7 +34,7 @@ enum login_proxy_failure_type { + LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, + /* Authentication failed to backend. The LOGIN/AUTH command reply was + already sent to the client. */ +- LOGIN_PROXY_FAILURE_TYPE_AUTH, ++ LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED, + /* Authentication failed with a temporary failure code. Attempting it + again might work. */ + LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL, +Index: trixie/src/pop3-login/pop3-proxy.c +=================================================================== +--- trixie.orig/src/pop3-login/pop3-proxy.c ++++ trixie/src/pop3-login/pop3-proxy.c +@@ -322,7 +322,7 @@ int pop3_proxy_parse_line(struct client + shouldn't be a real problem since of course everyone will + be using only Dovecot as their backend :) */ + enum login_proxy_failure_type failure_type = +- LOGIN_PROXY_FAILURE_TYPE_AUTH; ++ LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED; + if (!str_begins_with(line, "-ERR ")) { + client_send_reply(client, POP3_CMD_REPLY_ERROR, + AUTH_FAILED_MSG); +@@ -373,7 +373,7 @@ pop3_proxy_send_failure_reply(struct cli + /* [SYS/TEMP] prefix is already in the reason string */ + client_send_reply(client, POP3_CMD_REPLY_ERROR, reason); + break; +- case LOGIN_PROXY_FAILURE_TYPE_AUTH: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: + /* reply was already sent */ + break; + } +Index: trixie/src/submission-login/submission-proxy.c +=================================================================== +--- trixie.orig/src/submission-login/submission-proxy.c ++++ trixie/src/submission-login/submission-proxy.c +@@ -693,7 +693,7 @@ int submission_proxy_parse_line(struct c + shouldn't be a real problem since of course everyone will + be using only Dovecot as their backend :) */ + enum login_proxy_failure_type failure_type = +- LOGIN_PROXY_FAILURE_TYPE_AUTH; ++ LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED; + if ((status / 100) == 4) + failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL; + else if (!submission_proxy_handle_redirect( +@@ -749,7 +749,7 @@ submission_proxy_send_failure_reply(stru + i_assert(subm_client->proxy_reply != NULL); + smtp_server_reply_submit(subm_client->proxy_reply); + break; +- case LOGIN_PROXY_FAILURE_TYPE_AUTH: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: + /* reply was already sent */ + i_assert(cmd == NULL); + break; diff --git a/patches/0001-dsync-Avoid-potentially-excessive-data-stack-growth-.patch b/patches/0001-dsync-Avoid-potentially-excessive-data-stack-growth-.patch new file mode 100644 index 0000000..87c406e --- /dev/null +++ b/patches/0001-dsync-Avoid-potentially-excessive-data-stack-growth-.patch @@ -0,0 +1,40 @@ +From b40043ec2e45b79e9cb185009cffadfb1448da7e Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 24 Mar 2026 12:02:00 +0200 +Subject: [PATCH 1/2] dsync: Avoid potentially excessive data stack growth for + mailbox attribute sending + +The attribute value can be large. +--- + src/doveadm/dsync/dsync-ibc-stream.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/src/doveadm/dsync/dsync-ibc-stream.c b/src/doveadm/dsync/dsync-ibc-stream.c +index 472d1fc517..0b3038201d 100644 +--- a/src/doveadm/dsync/dsync-ibc-stream.c ++++ b/src/doveadm/dsync/dsync-ibc-stream.c +@@ -1492,12 +1492,12 @@ dsync_ibc_stream_send_mailbox_attribute(struct dsync_ibc *_ibc, + { + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; +- string_t *str = t_str_new(128); + char type[2]; + + if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES) + return; + ++ string_t *str = str_new(default_pool, 128); + str_append_c(str, items[ITEM_MAILBOX_ATTRIBUTE].chr); + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX_ATTRIBUTE); + +@@ -1538,6 +1538,7 @@ dsync_ibc_stream_send_mailbox_attribute(struct dsync_ibc *_ibc, + i_stream_ref(ibc->value_output); + (void)dsync_ibc_stream_send_value_stream(ibc); + } ++ str_free(&str); + } + + static enum dsync_ibc_recv_ret +-- +2.47.3 + diff --git a/patches/0001-global-Fix-spelling.patch b/patches/0001-global-Fix-spelling.patch new file mode 100644 index 0000000..d16a996 --- /dev/null +++ b/patches/0001-global-Fix-spelling.patch @@ -0,0 +1,34 @@ +Index: trixie/src/lib-compression/istream-zlib.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-zlib.c ++++ trixie/src/lib-compression/istream-zlib.c +@@ -32,7 +32,7 @@ struct zlib_istream { + bool header_read:1; + bool trailer_read:1; + bool zs_closed:1; +- bool starting_concated_output:1; ++ bool starting_concatenated_output:1; + }; + + static void i_stream_zlib_init(struct zlib_istream *zstream); +@@ -193,9 +193,9 @@ static ssize_t i_stream_zlib_read(struct + return -1; + } + /* Multiple gz streams concatenated together */ +- zstream->starting_concated_output = TRUE; ++ zstream->starting_concatenated_output = TRUE; + } +- if (zstream->starting_concated_output) { ++ if (zstream->starting_concatenated_output) { + /* make sure there actually is something in parent stream. + we don't want to reset the stream unless we actually see + some concatenated output. */ +@@ -217,7 +217,7 @@ static ssize_t i_stream_zlib_read(struct + zstream->header_read = FALSE; + zstream->trailer_read = FALSE; + zstream->crc32 = 0; +- zstream->starting_concated_output = FALSE; ++ zstream->starting_concatenated_output = FALSE; + + (void)inflateEnd(&zstream->zs); + i_stream_zlib_init(zstream); diff --git a/patches/0001-imap-Extract-side-channel-ostream-creation-into-help.patch b/patches/0001-imap-Extract-side-channel-ostream-creation-into-help.patch new file mode 100644 index 0000000..4470ae1 --- /dev/null +++ b/patches/0001-imap-Extract-side-channel-ostream-creation-into-help.patch @@ -0,0 +1,82 @@ +From da6075cd4803a36ae06ae0401184aeed128e4320 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 1 May 2026 17:40:08 +0000 +Subject: [PATCH 1/5] imap: Extract side-channel ostream creation into helper + +Add client_create_side_channel_output() and use it in cmd_compress() +in place of the inline channel-creation code. The helper will also be +used by the imap state import path to recreate the side channel after +unhibernation. No functional change. +--- + src/imap/cmd-compress.c | 10 ++-------- + src/imap/imap-client.c | 9 +++++++++ + src/imap/imap-client.h | 5 +++++ + 3 files changed, 16 insertions(+), 8 deletions(-) + +diff --git a/src/imap/cmd-compress.c b/src/imap/cmd-compress.c +index 363d050f47..18aa988892 100644 +--- a/src/imap/cmd-compress.c ++++ b/src/imap/cmd-compress.c +@@ -4,7 +4,6 @@ + #include "imap-commands.h" + #include "istream.h" + #include "ostream.h" +-#include "ostream-multiplex.h" + #include "iostream-rawlog.h" + #include "str.h" + #include "strescape.h" +@@ -92,13 +91,8 @@ bool cmd_compress(struct client_command_context *cmd) + /* Let imap-login process handle the COMPRESS. It's the one + that will send the tagged reply to the client. */ + client->compress_handler = handler; +- if (client->side_channel_output == NULL) { +- client->side_channel_output = +- o_stream_multiplex_add_channel( +- client->multiplex_output, 1); +- o_stream_set_no_error_handling( +- client->side_channel_output, TRUE); +- } ++ if (client->side_channel_output == NULL) ++ client_create_side_channel_output(client); + string_t *str = t_str_new(64); + str_append(str, "compress\t"); + str_append_tabescaped(str, handler->name); +diff --git a/src/imap/imap-client.c b/src/imap/imap-client.c +index b65a8c9fdf..4c8115f9a0 100644 +--- a/src/imap/imap-client.c ++++ b/src/imap/imap-client.c +@@ -678,6 +678,15 @@ void client_send_tagline(struct client_command_context *cmd, const char *data) + cmd->client->v.send_tagline(cmd, data); + } + ++void client_create_side_channel_output(struct client *client) ++{ ++ i_assert(client->multiplex_output != NULL); ++ i_assert(client->side_channel_output == NULL); ++ client->side_channel_output = ++ o_stream_multiplex_add_channel(client->multiplex_output, 1); ++ o_stream_set_no_error_handling(client->side_channel_output, TRUE); ++} ++ + static void + client_default_send_tagline(struct client_command_context *cmd, const char *data) + { +diff --git a/src/imap/imap-client.h b/src/imap/imap-client.h +index fd1153d97f..a4f2e4f3fb 100644 +--- a/src/imap/imap-client.h ++++ b/src/imap/imap-client.h +@@ -287,6 +287,11 @@ void client_add_istream_prefix(struct client *client, + const unsigned char *data, size_t size); + void client_destroy(struct client *client, const char *reason) ATTR_NULL(2); + ++/* Add the side-channel ostream used to send commands (e.g. dict_reset) back ++ to the imap-login proxy. Must only be called when multiplex_output is set ++ and the channel hasn't been created yet. */ ++void client_create_side_channel_output(struct client *client); ++ + /* Disconnect client connection */ + void client_disconnect(struct client *client, const char *reason); + void client_disconnect_with_error(struct client *client, +-- +2.47.3 + diff --git a/patches/0001-imap-Restrict-COMPRESS-to-DEFLATE-only.patch b/patches/0001-imap-Restrict-COMPRESS-to-DEFLATE-only.patch new file mode 100644 index 0000000..c3bfd4b --- /dev/null +++ b/patches/0001-imap-Restrict-COMPRESS-to-DEFLATE-only.patch @@ -0,0 +1,67 @@ +From 18f076b165d480cfdf20d81b5683ce7929f02eb4 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sat, 4 Jul 2026 21:57:15 +0000 +Subject: [PATCH 1/3] imap: Restrict COMPRESS to DEFLATE only + +cmd_compress() and the imap-login proxy side channel looked up any +registered compression handler by name, so a client could request e.g. +COMPRESS ZSTD even though only COMPRESS=DEFLATE is advertised (RFC 4978). + +Non-DEFLATE algorithms can require far more memory to decompress (a zstd +decoder alone can hold a 16+ MB window per connection versus ~40 KB for +inflate). This matters now that imap-login handles COMPRESS for proxied +connections: an attacker opening many connections could drive the shared +login process into its vsz_limit and crash it, affecting all clients on +that process. + +Only accept DEFLATE on both the backend and the login proxy path. +--- + src/imap-login/imap-proxy.c | 11 ++++++----- + src/imap/cmd-compress.c | 8 ++++++-- + 2 files changed, 12 insertions(+), 7 deletions(-) + +Index: trixie/src/imap-login/imap-proxy.c +=================================================================== +--- trixie.orig/src/imap-login/imap-proxy.c ++++ trixie/src/imap-login/imap-proxy.c +@@ -606,13 +606,14 @@ proxy_side_cmd_compress(struct client *c + login_proxy_get_client_ostream(client->login_proxy); + const struct compression_handler *handler; + int ret = compression_lookup_handler(t_str_lcase(args[0]), &handler); +- if (ret <= 0) { +- /* IMAP backend normally checks this already. If we get here, +- there is a mismatch between what algorithms proxy and +- backend supports. */ ++ /* Only DEFLATE is permitted. The IMAP backend restricts this already; ++ enforce it here too, since imap-login handles COMPRESS for proxied ++ connections and non-DEFLATE algorithms can be far more memory-heavy ++ to decompress. */ ++ if (ret <= 0 || strcmp(handler->name, "deflate") != 0) { + o_stream_nsend_str(client_output, t_strdup_printf( + "%s NO %s compression mechanism\r\n", args[1], +- ret == 0 ? "Unsupported" : "Unknown")); ++ ret < 0 ? "Unknown" : "Unsupported")); + return 0; + } + +Index: trixie/src/imap/cmd-compress.c +=================================================================== +--- trixie.orig/src/imap/cmd-compress.c ++++ trixie/src/imap/cmd-compress.c +@@ -67,10 +67,14 @@ bool cmd_compress(struct client_command_ + return TRUE; + } + int ret = compression_lookup_handler(t_str_lcase(mechanism), &handler); +- if (ret <= 0) { ++ /* Only DEFLATE is advertised (RFC 4978) and accepted here. Other ++ algorithms (e.g. zstd) can require far more memory to decompress, ++ which especially matters now that imap-login can handle COMPRESS for ++ proxied connections and an attacker could open many of them. */ ++ if (ret <= 0 || strcmp(handler->name, "deflate") != 0) { + const char * tagline = + t_strdup_printf("NO %s compression mechanism", +- ret == 0 ? "Unsupported" : "Unknown"); ++ ret < 0 ? "Unknown" : "Unsupported"); + client_send_tagline(cmd, tagline); + return TRUE; + } diff --git a/patches/0001-imap-hibernate-Fix-out-of-bounds-read-when-parsing-D.patch b/patches/0001-imap-hibernate-Fix-out-of-bounds-read-when-parsing-D.patch new file mode 100644 index 0000000..846b310 --- /dev/null +++ b/patches/0001-imap-hibernate-Fix-out-of-bounds-read-when-parsing-D.patch @@ -0,0 +1,35 @@ +From b4da247c728ecddc125c2d4021b9b7f70ff1f549 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 16 Apr 2026 23:12:13 +0200 +Subject: [PATCH 1/2] imap-hibernate: Fix out-of-bounds read when parsing DONE + command tag + +The tag-skipping loop was missing a size>0 guard, so a malformed +DONE command with no space/CR/tab terminator after the tag would +read one byte past the end of the buffer. Also add a \0 check to +stop on embedded null bytes, which are not valid tag characters. +--- + src/imap-hibernate/imap-client.c | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/src/imap-hibernate/imap-client.c b/src/imap-hibernate/imap-client.c +index 5c11dd1dde..2110fe8e8e 100644 +--- a/src/imap-hibernate/imap-client.c ++++ b/src/imap-hibernate/imap-client.c +@@ -362,9 +362,11 @@ imap_client_input_parse(const unsigned char *data, size_t size, const char **tag + tag_start = data; + + /* skip over tag */ +- while(data[0] != ' ' && ++ while(size > 0 && ++ data[0] != ' ' && + data[0] != '\r' && +- data[0] != '\t' ) { data++; size--; } ++ data[0] != '\t' && ++ data[0] != '\0') { data++; size--; } + + tag_end = data; + +-- +2.47.3 + diff --git a/patches/0001-imap-login-Add-comments-to-internal-ID-command-param.patch b/patches/0001-imap-login-Add-comments-to-internal-ID-command-param.patch new file mode 100644 index 0000000..c714df5 --- /dev/null +++ b/patches/0001-imap-login-Add-comments-to-internal-ID-command-param.patch @@ -0,0 +1,33 @@ +From 277e5c880a7f6a7741e717036570812910fff7b7 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 2 Feb 2026 11:44:58 +0200 +Subject: [PATCH] imap-login: Add comments to internal ID command parameter + handling + +--- + src/imap-login/imap-login-cmd-id.c | 9 +++++++-- + 1 file changed, 7 insertions(+), 2 deletions(-) + +diff --git a/src/imap-login/imap-login-cmd-id.c b/src/imap-login/imap-login-cmd-id.c +index 1cd30bd1bc..e51e9c1e72 100644 +--- a/src/imap-login/imap-login-cmd-id.c ++++ b/src/imap-login/imap-login-cmd-id.c +@@ -183,8 +183,13 @@ static bool cmd_id_handle_keyvalue(struct imap_client *client, + imap_id_param_handler_find(key); + bool is_login_id_param = handler != NULL; + +- if (is_login_id_param && client->common.connection_trusted && +- !client->id_logged && value != NULL) { ++ if (!is_login_id_param) { ++ /* not an internal key */ ++ } else if (client->id_logged) { ++ /* using ID multiple times - ignore */ ++ } else if (value == NULL) { ++ /* there should have been a value - ignore */ ++ } else if (client->common.connection_trusted) { + if (!handler->callback(client->cmd_id->params, key, value)) { + e_debug(client->common.event, + "Client sent invalid ID parameter '%s'", key); +-- +2.47.3 + diff --git a/patches/0001-imap-login-Fix-excessive-memory-growth-with-pre-logi.patch b/patches/0001-imap-login-Fix-excessive-memory-growth-with-pre-logi.patch new file mode 100644 index 0000000..12289ee --- /dev/null +++ b/patches/0001-imap-login-Fix-excessive-memory-growth-with-pre-logi.patch @@ -0,0 +1,86 @@ +From 4f3471057a754d93af368527e08af3db3d3e82c9 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sat, 4 Jul 2026 16:04:04 +0000 +Subject: [PATCH] imap-login: Fix excessive memory growth with pre-login ID + command + +The pre-login ID parser reset the per-argument line and list limits after +every argument, so nothing bounded the number of key/value pairs. Each +external key grew log_reply and added a distinct event field (a linear +scan, making it O(N^2)), allowing a pre-auth CPU/memory amplification. + +Account at most 30 pairs (per RFC 2971) into the logging and client_id +bookkeeping. Internal x-* parameter handlers still run for pairs beyond +the limit, since a trusted proxy may legitimately forward more fields. +--- + src/imap-login/imap-login-client.h | 4 ++++ + src/imap-login/imap-login-cmd-id.c | 20 ++++++++++++++++++-- + 2 files changed, 22 insertions(+), 2 deletions(-) + +Index: trixie/src/imap-login/imap-login-client.h +=================================================================== +--- trixie.orig/src/imap-login/imap-login-client.h ++++ trixie/src/imap-login/imap-login-client.h +@@ -53,6 +53,10 @@ struct imap_client_cmd_id { + struct event *params_event; + struct imap_id_params *params; + string_t *log_reply; ++ /* Number of external (non internal x-*) key/value pairs accounted so ++ far. Used to bound the pre-login accounting (logging, event fields, ++ client_id) done per external pair. */ ++ unsigned int processed_pairs_count; + }; + + struct imap_client { +Index: trixie/src/imap-login/imap-login-cmd-id.c +=================================================================== +--- trixie.orig/src/imap-login/imap-login-cmd-id.c ++++ trixie/src/imap-login/imap-login-cmd-id.c +@@ -9,6 +9,12 @@ + #include "imap-login-settings.h" + #include "imap-login-client.h" + ++/* RFC 2971 says a client SHOULD NOT send more than 30 field-value pairs. ++ Allow that many to be accounted for (logged / kept in client_id); pairs ++ beyond this are still parsed and internal x-* handlers still run, but they ++ are not added to the logging/client_id accounting that grows per pair. */ ++#define IMAP_ID_MAX_ACCOUNTED_PAIRS 30 ++ + struct imap_id_params_forward { + const char *key; + const char *value; +@@ -183,6 +189,16 @@ static bool cmd_id_handle_keyvalue(struc + imap_id_param_handler_find(key); + bool is_login_id_param = handler != NULL; + ++ /* Only external pairs are logged / kept in client_id, and only those ++ grow per-pair with count, so only they need bounding to avoid a ++ pre-login CPU/memory DoS. Internal x-* handlers must keep running ++ regardless (e.g. a proxy may legitimately forward more than 30 ++ fields) and are not counted against the limit. */ ++ bool account_pair = !is_login_id_param && ++ client->cmd_id->processed_pairs_count < IMAP_ID_MAX_ACCOUNTED_PAIRS; ++ if (account_pair) ++ client->cmd_id->processed_pairs_count++; ++ + if (!is_login_id_param) { + /* not an internal key */ + } else if (client->id_logged) { +@@ -197,7 +213,7 @@ static bool cmd_id_handle_keyvalue(struc + } + } + +- if (client->set->imap_id_retain && !is_login_id_param && ++ if (account_pair && client->set->imap_id_retain && + (client->common.client_id == NULL || + str_len(client->common.client_id) + kvlen < LOGIN_MAX_CLIENT_ID_LEN)) { + if (client->common.client_id == NULL) { +@@ -213,7 +229,7 @@ static bool cmd_id_handle_keyvalue(struc + imap_append_quoted(client->common.client_id, value); + } + +- if (!is_login_id_param) ++ if (account_pair) + imap_id_add_log_entry(log_entry, key, value); + return TRUE; + } diff --git a/patches/0001-lda-Fix-using-USER-environment-if-d-hasn-t-been-spec.patch b/patches/0001-lda-Fix-using-USER-environment-if-d-hasn-t-been-spec.patch new file mode 100644 index 0000000..2d55416 --- /dev/null +++ b/patches/0001-lda-Fix-using-USER-environment-if-d-hasn-t-been-spec.patch @@ -0,0 +1,38 @@ +From 0d490d6453b4779bc711f1b998e6d09ae2aab8a8 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 26 May 2025 09:37:35 +0300 +Subject: [PATCH] lda: Fix using USER environment if -d hasn't been specified + +This became broken at some point. +--- + src/lda/main.c | 10 +++++++--- + 1 file changed, 7 insertions(+), 3 deletions(-) + +Index: dovecot/src/lda/main.c +=================================================================== +--- dovecot.orig/src/lda/main.c ++++ dovecot/src/lda/main.c +@@ -453,9 +453,6 @@ int main(int argc, char *argv[]) + i_fatal_status(EX_USAGE, "Unknown argument: %s", argv[optind]); + } + +- if (master_service_settings_read_simple(master_service, &error) < 0) +- i_fatal("%s", error); +- + process_euid = geteuid(); + if ((service_flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) != 0) + ; +@@ -485,6 +482,13 @@ int main(int argc, char *argv[]) + i_fatal_status(EX_USAGE, + "destination user parameter (-d user) not given"); + } ++ struct master_service_settings_input set_input = { ++ .preserve_user = TRUE, ++ }; ++ struct master_service_settings_output set_output; ++ if (master_service_settings_read(master_service, &set_input, ++ &set_output, &error) < 0) ++ i_fatal("%s", error); + master_service_init_finish(master_service); + + dinput.mail_from = mail_from; diff --git a/patches/0001-lib-Add-XXH64-hash-implementation.patch b/patches/0001-lib-Add-XXH64-hash-implementation.patch new file mode 100644 index 0000000..3584708 --- /dev/null +++ b/patches/0001-lib-Add-XXH64-hash-implementation.patch @@ -0,0 +1,331 @@ +From 972acc9f9e73a8bc5b029991fb3752d9d16ef632 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 13 Apr 2026 12:30:03 +0200 +Subject: [PATCH 01/12] lib: Add XXH64 hash implementation + +Adds a streaming XXH64 (non-cryptographic) hash with init/loop/result +API, a one-shot xxh64_data(), and an xxh64_to_32() XOR-fold inline. +Registered in hash_methods[] for use via the generic hash_method API. +--- + src/lib/Makefile.am | 2 + + src/lib/hash-method.c | 2 + + src/lib/test-hash-method.c | 33 +++++++ + src/lib/xxh64.c | 184 +++++++++++++++++++++++++++++++++++++ + src/lib/xxh64.h | 30 ++++++ + 5 files changed, 251 insertions(+) + create mode 100644 src/lib/xxh64.c + create mode 100644 src/lib/xxh64.h + +diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am +index 6da8a308e5..eb9b9ddda3 100644 +--- a/src/lib/Makefile.am ++++ b/src/lib/Makefile.am +@@ -177,6 +177,7 @@ liblib_la_SOURCES = \ + sha1.c \ + sha2.c \ + sha3.c \ ++ xxh64.c \ + sleep.c \ + sort.c \ + stats-dist.c \ +@@ -337,6 +338,7 @@ headers = \ + sha1.h \ + sha2.h \ + sha3.h \ ++ xxh64.h \ + sleep.h \ + sort.h \ + stats-dist.h \ +diff --git a/src/lib/hash-method.c b/src/lib/hash-method.c +index d7a6bed0ea..bdeefbadc6 100644 +--- a/src/lib/hash-method.c ++++ b/src/lib/hash-method.c +@@ -6,6 +6,7 @@ + #include "sha1.h" + #include "sha2.h" + #include "sha3.h" ++#include "xxh64.h" + #include "hash-method.h" + + const struct hash_method *hash_method_lookup(const char *name) +@@ -93,6 +94,7 @@ const struct hash_method *hash_methods[] = { + &hash_method_sha512, + &hash_method_sha3_256, + &hash_method_sha3_512, ++ &hash_method_xxh64, + &hash_method_size, + NULL + }; +diff --git a/src/lib/test-hash-method.c b/src/lib/test-hash-method.c +index 6592ec90e6..6384dc99ca 100644 +--- a/src/lib/test-hash-method.c ++++ b/src/lib/test-hash-method.c +@@ -425,6 +425,39 @@ static void test_hash_methods_fips() { + "\x6d\xf6\x54\x5a\x1c\xe8\xba\x00", + 512 / 8, + }, ++ /* xxh64 reference vectors (seed=0) */ ++ { "xxh64", ++ "", ++ 0, ++ 1, ++ "\xef\x46\xdb\x37\x51\xd8\xe9\x99", ++ 64 / 8 ++ }, ++ { "xxh64", ++ "abc", ++ 3, ++ 1, ++ "\x44\xbc\x2c\xf5\xad\x77\x09\x99", ++ 64 / 8 ++ }, ++ /* 62 bytes: exercises the >= 32-byte accumulator path */ ++ { "xxh64", ++ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef" ++ "ghijklmnopqrstuvwxyz0123456789", ++ 62, ++ 1, ++ "\xba\x76\x1a\x9a\xa8\x0e\x0d\x36", ++ 64 / 8 ++ }, ++ /* 16 bytes × 3 rounds: exercises chunked input across the 32-byte block boundary */ ++ { "xxh64", ++ "\x00\x01\x02\x03\x04\x05\x06\x07" ++ "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", ++ 16, ++ 3, ++ "\x32\x0d\xcd\xab\xd8\x29\x7a\x9e", ++ 64 / 8 ++ }, + }; + + for(size_t i = 0; i < N_ELEMENTS(test_vectors); i++) { +diff --git a/src/lib/xxh64.c b/src/lib/xxh64.c +new file mode 100644 +index 0000000000..1c17833afd +--- /dev/null ++++ b/src/lib/xxh64.c +@@ -0,0 +1,184 @@ ++/* Copyright (c) 2025 Dovecot authors, see the included COPYING file */ ++ ++/* XXHash64 - Fast non-cryptographic hash function. ++ * Based on Yann Collet's public domain XXHash64 algorithm. ++ * Reference: https://github.com/Cyan4973/xxHash/blob/dev/doc/xxhash_spec.md ++ */ ++ ++#include "lib.h" ++#include "xxh64.h" ++ ++#define XXH64_PRIME1 UINT64_C(0x9E3779B185EBCA87) ++#define XXH64_PRIME2 UINT64_C(0xC2B2AE3D27D4EB4F) ++#define XXH64_PRIME3 UINT64_C(0x165667B19E3779F9) ++#define XXH64_PRIME4 UINT64_C(0x85EBCA77C2B27589) ++#define XXH64_PRIME5 UINT64_C(0x27D4EB2F165667C5) ++ ++#define XXH64_ROTL(x, r) (((x) << (r)) | ((x) >> (64 - (r)))) ++ ++static inline uint64_t xxh64_read64(const void *p) ++{ ++ uint64_t v; ++ memcpy(&v, p, sizeof(v)); ++ return v; ++} ++ ++static inline uint32_t xxh64_read32(const void *p) ++{ ++ uint32_t v; ++ memcpy(&v, p, sizeof(v)); ++ return v; ++} ++ ++static uint64_t ATTR_UNSIGNED_WRAPS xxh64_round(uint64_t acc, uint64_t input) ++{ ++ acc += input * XXH64_PRIME2; ++ acc = XXH64_ROTL(acc, 31); ++ acc *= XXH64_PRIME1; ++ return acc; ++} ++ ++static uint64_t ATTR_UNSIGNED_WRAPS xxh64_merge_acc(uint64_t h64, uint64_t acc) ++{ ++ acc = xxh64_round(0, acc); ++ h64 ^= acc; ++ h64 = h64 * XXH64_PRIME1 + XXH64_PRIME4; ++ return h64; ++} ++ ++void ATTR_UNSIGNED_WRAPS xxh64_init(struct xxh64_context *ctx, uint64_t seed) ++{ ++ ctx->seed = seed; ++ ctx->v1 = seed + XXH64_PRIME1 + XXH64_PRIME2; ++ ctx->v2 = seed + XXH64_PRIME2; ++ ctx->v3 = seed; ++ ctx->v4 = seed - XXH64_PRIME1; ++ ctx->total_len = 0; ++ ctx->buf_used = 0; ++} ++ ++void ATTR_UNSIGNED_WRAPS xxh64_loop(struct xxh64_context *ctx, const void *data, size_t size) ++{ ++ const unsigned char *p = data; ++ const unsigned char *end = p + size; ++ ++ ctx->total_len += size; ++ ++ if (ctx->buf_used + size < 32) { ++ memcpy(ctx->buf + ctx->buf_used, p, size); ++ ctx->buf_used += size; ++ return; ++ } ++ ++ if (ctx->buf_used > 0) { ++ size_t fill = 32 - ctx->buf_used; ++ memcpy(ctx->buf + ctx->buf_used, p, fill); ++ p += fill; ++ ctx->v1 = xxh64_round(ctx->v1, xxh64_read64(ctx->buf)); ++ ctx->v2 = xxh64_round(ctx->v2, xxh64_read64(ctx->buf + 8)); ++ ctx->v3 = xxh64_round(ctx->v3, xxh64_read64(ctx->buf + 16)); ++ ctx->v4 = xxh64_round(ctx->v4, xxh64_read64(ctx->buf + 24)); ++ ctx->buf_used = 0; ++ } ++ ++ while (p + 32 <= end) { ++ ctx->v1 = xxh64_round(ctx->v1, xxh64_read64(p)); ++ ctx->v2 = xxh64_round(ctx->v2, xxh64_read64(p + 8)); ++ ctx->v3 = xxh64_round(ctx->v3, xxh64_read64(p + 16)); ++ ctx->v4 = xxh64_round(ctx->v4, xxh64_read64(p + 24)); ++ p += 32; ++ } ++ ++ if (p < end) { ++ ctx->buf_used = (unsigned int)(end - p); ++ memcpy(ctx->buf, p, ctx->buf_used); ++ } ++} ++ ++uint64_t ATTR_UNSIGNED_WRAPS xxh64_result(struct xxh64_context *ctx) ++{ ++ const unsigned char *p = ctx->buf; ++ const unsigned char *end = p + ctx->buf_used; ++ uint64_t h64; ++ ++ if (ctx->total_len >= 32) { ++ h64 = XXH64_ROTL(ctx->v1, 1) + XXH64_ROTL(ctx->v2, 7) + ++ XXH64_ROTL(ctx->v3, 12) + XXH64_ROTL(ctx->v4, 18); ++ h64 = xxh64_merge_acc(h64, ctx->v1); ++ h64 = xxh64_merge_acc(h64, ctx->v2); ++ h64 = xxh64_merge_acc(h64, ctx->v3); ++ h64 = xxh64_merge_acc(h64, ctx->v4); ++ } else { ++ h64 = ctx->seed + XXH64_PRIME5; ++ } ++ ++ h64 += ctx->total_len; ++ ++ while (p + 8 <= end) { ++ h64 ^= xxh64_round(0, xxh64_read64(p)); ++ h64 = XXH64_ROTL(h64, 27) * XXH64_PRIME1 + XXH64_PRIME4; ++ p += 8; ++ } ++ if (p + 4 <= end) { ++ h64 ^= (uint64_t)xxh64_read32(p) * XXH64_PRIME1; ++ h64 = XXH64_ROTL(h64, 23) * XXH64_PRIME2 + XXH64_PRIME3; ++ p += 4; ++ } ++ while (p < end) { ++ h64 ^= (uint64_t)*p * XXH64_PRIME5; ++ h64 = XXH64_ROTL(h64, 11) * XXH64_PRIME1; ++ p++; ++ } ++ ++ /* Avalanche */ ++ h64 ^= h64 >> 33; ++ h64 *= XXH64_PRIME2; ++ h64 ^= h64 >> 29; ++ h64 *= XXH64_PRIME3; ++ h64 ^= h64 >> 32; ++ ++ return h64; ++} ++ ++uint64_t xxh64_data(const void *data, size_t size, uint64_t seed) ++{ ++ struct xxh64_context ctx; ++ ++ xxh64_init(&ctx, seed); ++ xxh64_loop(&ctx, data, size); ++ return xxh64_result(&ctx); ++} ++ ++static void xxh64_hash_init(void *context) ++{ ++ xxh64_init(context, 0); ++} ++ ++static void xxh64_hash_loop(void *context, const void *data, size_t size) ++{ ++ xxh64_loop(context, data, size); ++} ++ ++static void xxh64_hash_result(void *context, unsigned char *digest_r) ++{ ++ uint64_t hash = xxh64_result(context); ++ ++ digest_r[0] = (hash >> 56) & 0xff; ++ digest_r[1] = (hash >> 48) & 0xff; ++ digest_r[2] = (hash >> 40) & 0xff; ++ digest_r[3] = (hash >> 32) & 0xff; ++ digest_r[4] = (hash >> 24) & 0xff; ++ digest_r[5] = (hash >> 16) & 0xff; ++ digest_r[6] = (hash >> 8) & 0xff; ++ digest_r[7] = (hash >> 0) & 0xff; ++} ++ ++const struct hash_method hash_method_xxh64 = { ++ .name = "xxh64", ++ .block_size = 32, ++ .context_size = sizeof(struct xxh64_context), ++ .digest_size = XXH64_RESULTLEN, ++ .init = xxh64_hash_init, ++ .loop = xxh64_hash_loop, ++ .result = xxh64_hash_result, ++}; +diff --git a/src/lib/xxh64.h b/src/lib/xxh64.h +new file mode 100644 +index 0000000000..7beb4cafb4 +--- /dev/null ++++ b/src/lib/xxh64.h +@@ -0,0 +1,30 @@ ++#ifndef XXH64_H ++#define XXH64_H ++ ++#include "hash-method.h" ++ ++#define XXH64_RESULTLEN 8 ++ ++struct xxh64_context { ++ uint64_t seed; ++ uint64_t v1, v2, v3, v4; ++ uint64_t total_len; ++ unsigned char buf[32]; ++ unsigned int buf_used; ++}; ++ ++void xxh64_init(struct xxh64_context *ctx, uint64_t seed); ++void xxh64_loop(struct xxh64_context *ctx, const void *data, size_t size); ++uint64_t xxh64_result(struct xxh64_context *ctx); ++ ++uint64_t xxh64_data(const void *data, size_t size, uint64_t seed) ATTR_PURE; ++ ++/* XOR-fold the 64-bit hash to 32 bits */ ++static inline uint32_t xxh64_to_32(uint64_t hash) ++{ ++ return (uint32_t)(hash ^ (hash >> 32)); ++} ++ ++extern const struct hash_method hash_method_xxh64; ++ ++#endif +-- +2.47.3 + diff --git a/patches/0001-lib-Add-str_equals_timing_safe.patch b/patches/0001-lib-Add-str_equals_timing_safe.patch new file mode 100644 index 0000000..7a6c53f --- /dev/null +++ b/patches/0001-lib-Add-str_equals_timing_safe.patch @@ -0,0 +1,124 @@ +From 09237b986e2a5710a562a9750a6df1aef6d28e69 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 28 Apr 2026 01:50:56 +0300 +Subject: [PATCH 1/2] lib: Add str_equals_timing_safe() + +Constant-time string comparison that avoids the length leak in +str_equals_timing_almost_safe(). Compares HMAC-SHA256 digests of the +inputs (keyed with hash_iv) rather than the strings themselves, so +neither the contents nor the length of either input affects timing in +a way an attacker can exploit. +--- + src/lib/strfuncs.c | 29 +++++++++++++++++++++++++++++ + src/lib/strfuncs.h | 5 +++++ + src/lib/test-strfuncs.c | 27 +++++++++++++++++++++++++++ + 3 files changed, 61 insertions(+) + +Index: dovecot/src/lib/strfuncs.c +=================================================================== +--- dovecot.orig/src/lib/strfuncs.c ++++ dovecot/src/lib/strfuncs.c +@@ -7,6 +7,9 @@ + #include "printf-format-fix.h" + #include "strfuncs.h" + #include "array.h" ++#include "hash.h" ++#include "hmac.h" ++#include "sha2.h" + + #include + #include +@@ -627,6 +630,32 @@ bool str_equals_timing_almost_safe(const + return ret == 0; + } + ++bool str_equals_hash_timing_safe(const char *s1, const char *s2) ++{ ++ struct hmac_context ctx; ++ unsigned char digest1[SHA256_RESULTLEN]; ++ unsigned char digest2[SHA256_RESULTLEN]; ++ ++ /* Compare HMAC-SHA256 digests of the inputs rather than the inputs ++ themselves. The digest length is constant, so the subsequent ++ mem_equals_timing_safe() leaks no length information. The HMAC ++ times depend on the input lengths, but each side's length is either ++ a deployment constant (for the secret) or already known to the ++ attacker (for their own input), so neither leaks a useful signal. ++ hash_iv keys the HMAC to prevent precomputation. */ ++ hmac_init(&ctx, (const unsigned char *)&hash_iv, sizeof(hash_iv), ++ &hash_method_sha256); ++ hmac_update(&ctx, s1, strlen(s1)); ++ hmac_final(&ctx, digest1); ++ ++ hmac_init(&ctx, (const unsigned char *)&hash_iv, sizeof(hash_iv), ++ &hash_method_sha256); ++ hmac_update(&ctx, s2, strlen(s2)); ++ hmac_final(&ctx, digest2); ++ ++ return mem_equals_timing_safe(digest1, digest2, sizeof(digest1)); ++} ++ + size_t + str_match(const char *p1, const char *p2) + { +Index: dovecot/src/lib/strfuncs.h +=================================================================== +--- dovecot.orig/src/lib/strfuncs.h ++++ dovecot/src/lib/strfuncs.h +@@ -93,6 +93,11 @@ bool mem_equals_timing_safe(const void * + the string lengths are the same. If not, the length of the secret string may + be leaked, but otherwise the contents won't be. */ + bool str_equals_timing_almost_safe(const char *s1, const char *s2); ++/* Returns TRUE if the two strings are equal. Safe against timing attacks: ++ neither the contents nor the length of either string is leaked. ++ Implemented by HMAC-SHA256ing both inputs under a random per-process key ++ and comparing the fixed-length digests. */ ++bool str_equals_hash_timing_safe(const char *s1, const char *s2); + + size_t str_match(const char *p1, const char *p2) ATTR_PURE; + size_t str_match_icase(const char *p1, const char *p2) ATTR_PURE; +Index: dovecot/src/lib/test-strfuncs.c +=================================================================== +--- dovecot.orig/src/lib/test-strfuncs.c ++++ dovecot/src/lib/test-strfuncs.c +@@ -478,6 +478,32 @@ static void test_str_equals_timing_almos + test_end(); + } + ++static void test_str_equals_hash_timing_safe(void) ++{ ++ const struct { ++ const char *a, *b; ++ } tests[] = { ++ { "", "" }, ++ { "a", "a" }, ++ { "b", "a" }, ++ { "ab", "ab" }, ++ { "ab", "ba" }, ++ { "ab", "bc" }, ++ { "a", "" }, ++ { "a", "ab" }, ++ { "a", "abc" }, ++ { "ab", "abc" }, ++ }; ++ test_begin("str_equals_hash_timing_safe()"); ++ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) { ++ test_assert((strcmp(tests[i].a, tests[i].b) == 0) == ++ str_equals_hash_timing_safe(tests[i].a, tests[i].b)); ++ test_assert((strcmp(tests[i].a, tests[i].b) == 0) == ++ str_equals_hash_timing_safe(tests[i].b, tests[i].a)); ++ } ++ test_end(); ++} ++ + static void test_dec2str_buf(void) + { + const uintmax_t test_input[] = { +@@ -773,6 +799,7 @@ void test_strfuncs(void) + test_p_array_const_string_join(); + test_mem_equals_timing_safe(); + test_str_equals_timing_almost_safe(); ++ test_str_equals_hash_timing_safe(); + test_dec2str_buf(); + test_str_match(); + test_str_match_icase(); diff --git a/patches/0001-lib-compression-Don-t-calculate-crc32-if-using-zlib-.patch b/patches/0001-lib-compression-Don-t-calculate-crc32-if-using-zlib-.patch new file mode 100644 index 0000000..6f07d05 --- /dev/null +++ b/patches/0001-lib-compression-Don-t-calculate-crc32-if-using-zlib-.patch @@ -0,0 +1,45 @@ +From cac140a9e4e12485bde4348b8a8c94a7a96a6e5e Mon Sep 17 00:00:00 2001 +From: Michael M Slusarz +Date: Sat, 4 Oct 2025 12:04:31 -0600 +Subject: [PATCH] lib-compression: Don't calculate crc32 if using zlib in + deflate mode + +--- + src/lib-compression/istream-zlib.c | 8 +++++--- + src/lib-compression/ostream-zlib.c | 4 +++- + 2 files changed, 8 insertions(+), 4 deletions(-) + +Index: trixie/src/lib-compression/istream-zlib.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-zlib.c ++++ trixie/src/lib-compression/istream-zlib.c +@@ -264,9 +264,11 @@ static ssize_t i_stream_zlib_read(struct + ret = inflate(&zstream->zs, Z_SYNC_FLUSH); + + out_size -= zstream->zs.avail_out; +- zstream->crc32 = crc32_data_more(zstream->crc32, +- stream->w_buffer + stream->pos, +- out_size); ++ /* CRC32 is only needed for GZ trailer. */ ++ if (zstream->gz) ++ zstream->crc32 = crc32_data_more(zstream->crc32, ++ stream->w_buffer + stream->pos, ++ out_size); + stream->pos += out_size; + + size_t bytes_consumed = size - zstream->zs.avail_in; +Index: trixie/src/lib-compression/ostream-zlib.c +=================================================================== +--- trixie.orig/src/lib-compression/ostream-zlib.c ++++ trixie/src/lib-compression/ostream-zlib.c +@@ -219,7 +219,9 @@ o_stream_zlib_send_chunk(struct zlib_ost + } + size -= zs->avail_in; + +- zstream->crc = crc32_data_more(zstream->crc, data, size); ++ /* CRC32 is only needed for GZ trailer. */ ++ if (zstream->gz) ++ zstream->crc = crc32_data_more(zstream->crc, data, size); + zstream->bytes32 += size; + zstream->flushed = FALSE; + return size; diff --git a/patches/0001-lib-compression-istream-lz4-Ensure-uncompressed-chun.patch b/patches/0001-lib-compression-istream-lz4-Ensure-uncompressed-chun.patch new file mode 100644 index 0000000..dcbc6b6 --- /dev/null +++ b/patches/0001-lib-compression-istream-lz4-Ensure-uncompressed-chun.patch @@ -0,0 +1,98 @@ +From 86aba25a1159f7ff11480feec287647be78054d1 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 23 Feb 2026 13:52:36 +0200 +Subject: [PATCH] lib-compression: istream-lz4 - Ensure uncompressed chunk size + is not 0 + +--- + src/lib-compression/istream-lz4.c | 5 ++- + src/lib-compression/test-compression.c | 52 ++++++++++++++++++++++++++ + 2 files changed, 55 insertions(+), 2 deletions(-) + +Index: trixie/src/lib-compression/istream-lz4.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-lz4.c ++++ trixie/src/lib-compression/istream-lz4.c +@@ -77,9 +77,10 @@ static int i_stream_lz4_read_header(stru + zstream->max_uncompressed_chunk_size = + be32_to_cpu_unaligned(hdr->max_uncompressed_chunk_size); + buffer_set_used_size(zstream->chunk_buf, 0); +- if (zstream->max_uncompressed_chunk_size > ISTREAM_LZ4_CHUNK_SIZE) { ++ if (zstream->max_uncompressed_chunk_size == 0 || ++ zstream->max_uncompressed_chunk_size > ISTREAM_LZ4_CHUNK_SIZE) { + lz4_read_error(zstream, t_strdup_printf( +- "lz4 max chunk size too large (%u > %u)", ++ "invalid lz4 max chunk size (%u, max %u)", + zstream->max_uncompressed_chunk_size, + ISTREAM_LZ4_CHUNK_SIZE)); + zstream->istream.istream.stream_errno = EINVAL; +Index: trixie/src/lib-compression/test-compression.c +=================================================================== +--- trixie.orig/src/lib-compression/test-compression.c ++++ trixie/src/lib-compression/test-compression.c +@@ -1040,6 +1040,57 @@ static void test_lz4_small_header(void) + test_end(); + } + ++static const unsigned char lz4_chunk_crash_01[] = { ++ 0x44, 0x6f, 0x76, 0x65, 0x63, 0x6f, 0x74, 0x2d, 0x4c, 0x5a, 0x34, 0x0d, ++ 0x2a, 0x9b, 0xc5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x7a, ++ 0x32, 0x01, 0xff, 0xff, 0x00, 0x30 ++}; ++ ++static const unsigned char lz4_chunk_crash_02[] = { ++ 0x44, 0x6f, 0x76, 0x65, 0x63, 0x6f, 0x74, 0x2d, 0x4c, 0x5a, 0x34, 0x0d, ++ 0x2a, 0x9b, 0xc5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0x7a, ++ 0x32, 0x01, 0xff, 0xff, 0x00, 0x30 ++}; ++ ++static void test_lz4_chunk_size(void) ++{ ++ const struct compression_handler *lz4; ++ struct istream *file_input, *input; ++ ++ if (compression_lookup_handler("lz4", &lz4) <= 0) ++ return; /* not compiled in or unknown */ ++ ++ test_begin("lz4 chunk size"); ++ ++ file_input = test_istream_create_data(lz4_chunk_crash_01, ++ sizeof(lz4_chunk_crash_01)); ++ input = lz4->create_istream(file_input); ++ i_stream_unref(&file_input); ++ i_stream_read(input); ++ ++ test_assert(input->eof); ++ test_assert(input->stream_errno != 0); ++ const char *error = i_stream_get_error(input); ++ test_assert(strstr(error, "invalid lz4 max chunk size") != NULL); ++ ++ i_stream_unref(&input); ++ ++ file_input = test_istream_create_data(lz4_chunk_crash_02, ++ sizeof(lz4_chunk_crash_02)); ++ input = lz4->create_istream(file_input); ++ i_stream_unref(&file_input); ++ i_stream_read(input); ++ ++ test_assert(input->eof); ++ test_assert(input->stream_errno != 0); ++ error = i_stream_get_error(input); ++ test_assert(strstr(error, "invalid lz4 max chunk size") != NULL); ++ ++ i_stream_unref(&input); ++ ++ test_end(); ++} ++ + static void test_uncompress_file(const char *path) + { + const struct compression_handler *handler; +@@ -1163,6 +1214,7 @@ int main(int argc, char *argv[]) + test_gz_header, + test_gz_large_header, + test_lz4_small_header, ++ test_lz4_chunk_size, + test_compression_ext, + test_compression_deinit, + NULL diff --git a/patches/0001-lib-compression-istream-lz4-Try-again-if-no-data-was.patch b/patches/0001-lib-compression-istream-lz4-Try-again-if-no-data-was.patch new file mode 100644 index 0000000..dd97d3f --- /dev/null +++ b/patches/0001-lib-compression-istream-lz4-Try-again-if-no-data-was.patch @@ -0,0 +1,60 @@ +From f269b3d3051044aec31cef888fcf4c9ddd21c227 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 23 Feb 2026 14:09:02 +0200 +Subject: [PATCH] lib-compression: istream-lz4 - Try again if no data was + decompressed + +--- + src/lib-compression/istream-lz4.c | 3 ++- + src/lib-compression/test-compression.c | 17 +++++++++++++++++ + 2 files changed, 19 insertions(+), 1 deletion(-) + +Index: trixie/src/lib-compression/istream-lz4.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-lz4.c ++++ trixie/src/lib-compression/istream-lz4.c +@@ -198,7 +198,8 @@ static ssize_t i_stream_lz4_read(struct + lz4_read_error(zstream, "corrupted lz4 chunk"); + stream->istream.stream_errno = EINVAL; + return -1; +- } ++ } else if (ret == 0) ++ return i_stream_lz4_read(stream); + i_assert(ret > 0); + stream->pos += ret; + i_assert(stream->pos <= stream->buffer_size); +Index: trixie/src/lib-compression/test-compression.c +=================================================================== +--- trixie.orig/src/lib-compression/test-compression.c ++++ trixie/src/lib-compression/test-compression.c +@@ -1052,6 +1052,11 @@ static const unsigned char lz4_chunk_cra + 0x32, 0x01, 0xff, 0xff, 0x00, 0x30 + }; + ++static const unsigned char lz4_chunk_eof_03[] = { ++ 0x44, 0x6f, 0x76, 0x65, 0x63, 0x6f, 0x74, 0x2d, 0x4c, 0x5a, 0x34, 0x0d, ++ 0x2a, 0x9b, 0xc5, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, ++}; ++ + static void test_lz4_chunk_size(void) + { + const struct compression_handler *lz4; +@@ -1088,6 +1093,18 @@ static void test_lz4_chunk_size(void) + + i_stream_unref(&input); + ++ file_input = test_istream_create_data(lz4_chunk_eof_03, ++ sizeof(lz4_chunk_eof_03)); ++ input = lz4->create_istream(file_input); ++ i_stream_unref(&file_input); ++ i_stream_read(input); ++ ++ /* no error, but no content either */ ++ test_assert(input->eof); ++ test_assert(input->stream_errno == 0); ++ ++ i_stream_unref(&input); ++ + test_end(); + } + diff --git a/patches/0001-lib-compression-istream-zlib-Use-container_of-macro.patch b/patches/0001-lib-compression-istream-zlib-Use-container_of-macro.patch new file mode 100644 index 0000000..ca38266 --- /dev/null +++ b/patches/0001-lib-compression-istream-zlib-Use-container_of-macro.patch @@ -0,0 +1,56 @@ +From 09df3b5b8b970439ab6b1b617b344a21cb2c07fe Mon Sep 17 00:00:00 2001 +From: Stephan Bosch +Date: Tue, 14 Nov 2023 16:57:45 +0100 +Subject: [PATCH] lib-compression: istream-zlib - Use container_of() macro + +--- + src/lib-compression/istream-zlib.c | 12 ++++++++---- + 1 file changed, 8 insertions(+), 4 deletions(-) + +diff --git a/src/lib-compression/istream-zlib.c b/src/lib-compression/istream-zlib.c +index 93b3473071..0a1832a66d 100644 +--- a/src/lib-compression/istream-zlib.c ++++ b/src/lib-compression/istream-zlib.c +@@ -40,7 +40,8 @@ static void i_stream_zlib_init(struct zlib_istream *zstream); + static void i_stream_zlib_close(struct iostream_private *stream, + bool close_parent) + { +- struct zlib_istream *zstream = (struct zlib_istream *)stream; ++ struct zlib_istream *zstream = ++ container_of(stream, struct zlib_istream, istream.iostream); + + if (!zstream->zs_closed) { + (void)inflateEnd(&zstream->zs); +@@ -160,7 +161,8 @@ static int i_stream_zlib_read_trailer(struct zlib_istream *zstream) + + static ssize_t i_stream_zlib_read(struct istream_private *stream) + { +- struct zlib_istream *zstream = (struct zlib_istream *)stream; ++ struct zlib_istream *zstream = ++ container_of(stream, struct zlib_istream, istream); + const unsigned char *data; + uoff_t high_offset; + size_t size, out_size; +@@ -362,7 +364,8 @@ static void i_stream_zlib_reset(struct zlib_istream *zstream) + static void + i_stream_zlib_seek(struct istream_private *stream, uoff_t v_offset, bool mark) + { +- struct zlib_istream *zstream = (struct zlib_istream *) stream; ++ struct zlib_istream *zstream = ++ container_of(stream, struct zlib_istream, istream); + + if (i_stream_nonseekable_try_seek(stream, v_offset)) + return; +@@ -378,7 +381,8 @@ i_stream_zlib_seek(struct istream_private *stream, uoff_t v_offset, bool mark) + + static void i_stream_zlib_sync(struct istream_private *stream) + { +- struct zlib_istream *zstream = (struct zlib_istream *) stream; ++ struct zlib_istream *zstream = ++ container_of(stream, struct zlib_istream, istream); + const struct stat *st; + + if (i_stream_stat(stream->parent, FALSE, &st) == 0) { +-- +2.47.3 + diff --git a/patches/0001-lib-compression-wrap-_read-in-for-loop-reindent-only.patch b/patches/0001-lib-compression-wrap-_read-in-for-loop-reindent-only.patch new file mode 100644 index 0000000..59cbc99 --- /dev/null +++ b/patches/0001-lib-compression-wrap-_read-in-for-loop-reindent-only.patch @@ -0,0 +1,611 @@ +From aa8e26f7879895ff55bc247fb2594ffb3c34f4bc Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 10 Jun 2026 11:53:39 +0000 +Subject: [PATCH 1/4] lib-compression: wrap *_read() in for(;;) loop (reindent + only) + +Prepare for the next commit: wrap the body of i_stream_lz4_read(), +i_stream_bzlib_read(), and i_stream_zlib_read() in a for(;;) loop. +No logic change; all paths still return on the first iteration. +Separating the indent churn makes the actual fix easier to review. +--- + src/lib-compression/istream-bzlib.c | 158 ++++++++--------- + src/lib-compression/istream-lz4.c | 114 ++++++------ + src/lib-compression/istream-zlib.c | 260 ++++++++++++++-------------- + 3 files changed, 269 insertions(+), 263 deletions(-) + +Index: trixie/src/lib-compression/istream-bzlib.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-bzlib.c ++++ trixie/src/lib-compression/istream-bzlib.c +@@ -51,88 +51,90 @@ static ssize_t i_stream_bzlib_read(struc + size_t size, out_size; + int ret; + +- high_offset = stream->istream.v_offset + (stream->pos - stream->skip); +- if (zstream->eof_offset == high_offset) { +- stream->istream.eof = TRUE; +- return -1; +- } +- +- if (!zstream->marked) { +- if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size)) +- return -2; /* buffer full */ +- } else { +- /* try to avoid compressing, so we can quickly seek backwards */ +- if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size)) +- return -2; /* buffer full */ +- } ++ for (;;) { ++ high_offset = stream->istream.v_offset + (stream->pos - stream->skip); ++ if (zstream->eof_offset == high_offset) { ++ stream->istream.eof = TRUE; ++ return -1; ++ } + +- if (i_stream_read_more(stream->parent, &data, &size) < 0) { +- if (stream->parent->stream_errno != 0) { +- stream->istream.stream_errno = +- stream->parent->stream_errno; ++ if (!zstream->marked) { ++ if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size)) ++ return -2; /* buffer full */ + } else { +- i_assert(stream->parent->eof); +- bzlib_read_error(zstream, "unexpected EOF"); +- if (!zstream->hdr_read) +- stream->istream.stream_errno = EINVAL; +- else +- stream->istream.stream_errno = EPIPE; ++ /* try to avoid compressing, so we can quickly seek backwards */ ++ if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size)) ++ return -2; /* buffer full */ ++ } ++ ++ if (i_stream_read_more(stream->parent, &data, &size) < 0) { ++ if (stream->parent->stream_errno != 0) { ++ stream->istream.stream_errno = ++ stream->parent->stream_errno; ++ } else { ++ i_assert(stream->parent->eof); ++ bzlib_read_error(zstream, "unexpected EOF"); ++ if (!zstream->hdr_read) ++ stream->istream.stream_errno = EINVAL; ++ else ++ stream->istream.stream_errno = EPIPE; ++ } ++ return -1; ++ } ++ if (size == 0) { ++ /* no more input */ ++ i_assert(!stream->istream.blocking); ++ return 0; + } +- return -1; +- } +- if (size == 0) { +- /* no more input */ +- i_assert(!stream->istream.blocking); +- return 0; +- } + +- zstream->zs.next_in = (char *)data; +- zstream->zs.avail_in = size; ++ zstream->zs.next_in = (char *)data; ++ zstream->zs.avail_in = size; + +- zstream->zs.next_out = (char *)stream->w_buffer + stream->pos; +- zstream->zs.avail_out = out_size; +- ret = BZ2_bzDecompress(&zstream->zs); +- zstream->hdr_read = TRUE; +- +- out_size -= zstream->zs.avail_out; +- stream->pos += out_size; +- +- i_stream_skip(stream->parent, size - zstream->zs.avail_in); +- +- switch (ret) { +- case BZ_OK: +- break; +- case BZ_PARAM_ERROR: +- i_unreached(); +- case BZ_DATA_ERROR: +- bzlib_read_error(zstream, "corrupted data"); +- stream->istream.stream_errno = EINVAL; +- return -1; +- case BZ_DATA_ERROR_MAGIC: +- bzlib_read_error(zstream, +- "wrong magic in header (not bz2 file?)"); +- stream->istream.stream_errno = EINVAL; +- return -1; +- case BZ_MEM_ERROR: +- i_fatal_status(FATAL_OUTOFMEM, "bzlib.read(%s): Out of memory", +- i_stream_get_name(&stream->istream)); +- case BZ_STREAM_END: +- zstream->eof_offset = stream->istream.v_offset + +- (stream->pos - stream->skip); +- stream->cached_stream_size = zstream->eof_offset; +- if (out_size == 0) { +- stream->istream.eof = TRUE; ++ zstream->zs.next_out = (char *)stream->w_buffer + stream->pos; ++ zstream->zs.avail_out = out_size; ++ ret = BZ2_bzDecompress(&zstream->zs); ++ zstream->hdr_read = TRUE; ++ ++ out_size -= zstream->zs.avail_out; ++ stream->pos += out_size; ++ ++ i_stream_skip(stream->parent, size - zstream->zs.avail_in); ++ ++ switch (ret) { ++ case BZ_OK: ++ break; ++ case BZ_PARAM_ERROR: ++ i_unreached(); ++ case BZ_DATA_ERROR: ++ bzlib_read_error(zstream, "corrupted data"); ++ stream->istream.stream_errno = EINVAL; + return -1; ++ case BZ_DATA_ERROR_MAGIC: ++ bzlib_read_error(zstream, ++ "wrong magic in header (not bz2 file?)"); ++ stream->istream.stream_errno = EINVAL; ++ return -1; ++ case BZ_MEM_ERROR: ++ i_fatal_status(FATAL_OUTOFMEM, "bzlib.read(%s): Out of memory", ++ i_stream_get_name(&stream->istream)); ++ case BZ_STREAM_END: ++ zstream->eof_offset = stream->istream.v_offset + ++ (stream->pos - stream->skip); ++ stream->cached_stream_size = zstream->eof_offset; ++ if (out_size == 0) { ++ stream->istream.eof = TRUE; ++ return -1; ++ } ++ break; ++ default: ++ i_fatal("BZ2_bzDecompress() failed with %d", ret); + } +- break; +- default: +- i_fatal("BZ2_bzDecompress() failed with %d", ret); +- } +- if (out_size == 0) { +- /* read more input */ +- return i_stream_bzlib_read(stream); ++ if (out_size == 0) { ++ /* read more input */ ++ return i_stream_bzlib_read(stream); ++ } ++ return out_size; + } +- return out_size; + } + + static void i_stream_bzlib_init(struct bzlib_istream *zstream) +Index: trixie/src/lib-compression/istream-lz4.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-lz4.c ++++ trixie/src/lib-compression/istream-lz4.c +@@ -149,67 +149,69 @@ static ssize_t i_stream_lz4_read(struct + zstream->header_read = TRUE; + } + +- if (zstream->chunk_left == 0) { +- while ((ret = i_stream_lz4_read_chunk_header(zstream)) == 0) { +- if (!stream->istream.blocking) +- return 0; ++ for (;;) { ++ if (zstream->chunk_left == 0) { ++ while ((ret = i_stream_lz4_read_chunk_header(zstream)) == 0) { ++ if (!stream->istream.blocking) ++ return 0; ++ } ++ if (ret < 0) ++ return ret; + } +- if (ret < 0) +- return ret; +- } + +- /* read the whole compressed chunk into memory */ +- while (zstream->chunk_left > 0 && +- (ret = i_stream_read_more(zstream->istream.parent, &data, &size)) > 0) { +- if (size > zstream->chunk_left) +- size = zstream->chunk_left; +- buffer_append(zstream->chunk_buf, data, size); +- i_stream_skip(zstream->istream.parent, size); +- zstream->chunk_left -= size; +- } +- if (zstream->chunk_left > 0) { +- if (ret == -1 && zstream->istream.parent->stream_errno == 0) { +- lz4_read_error(zstream, "truncated lz4 chunk"); +- stream->istream.stream_errno = EPIPE; +- return -1; ++ /* read the whole compressed chunk into memory */ ++ while (zstream->chunk_left > 0 && ++ (ret = i_stream_read_more(zstream->istream.parent, &data, &size)) > 0) { ++ if (size > zstream->chunk_left) ++ size = zstream->chunk_left; ++ buffer_append(zstream->chunk_buf, data, size); ++ i_stream_skip(zstream->istream.parent, size); ++ zstream->chunk_left -= size; + } +- zstream->istream.istream.stream_errno = +- zstream->istream.parent->stream_errno; +- i_assert(ret != 0 || !stream->istream.blocking); +- return ret; +- } +- /* if we already have max_buffer_size amount of data, fail here */ +- if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream)) +- return -2; +- if (i_stream_get_data_size(zstream->istream.parent) > 0) { +- /* Parent stream was only partially consumed. Set the stream's +- IO as pending to avoid hangs. */ +- i_stream_set_input_pending(&zstream->istream.istream, TRUE); +- } +- /* allocate enough space for the old data and the new +- decompressed chunk. we don't know the original compressed size, +- so just allocate the max amount of memory. */ +- void *dest = i_stream_alloc(stream, zstream->max_uncompressed_chunk_size); +- ret = LZ4_decompress_safe(zstream->chunk_buf->data, dest, +- zstream->chunk_buf->used, +- zstream->max_uncompressed_chunk_size); +- i_assert(ret <= (int)zstream->max_uncompressed_chunk_size); +- if (ret < 0) { +- lz4_read_error(zstream, "corrupted lz4 chunk"); +- stream->istream.stream_errno = EINVAL; +- return -1; +- } else if (ret == 0) +- return i_stream_lz4_read(stream); +- i_assert(ret > 0); +- stream->pos += ret; +- i_assert(stream->pos <= stream->buffer_size); ++ if (zstream->chunk_left > 0) { ++ if (ret == -1 && zstream->istream.parent->stream_errno == 0) { ++ lz4_read_error(zstream, "truncated lz4 chunk"); ++ stream->istream.stream_errno = EPIPE; ++ return -1; ++ } ++ zstream->istream.istream.stream_errno = ++ zstream->istream.parent->stream_errno; ++ i_assert(ret != 0 || !stream->istream.blocking); ++ return ret; ++ } ++ /* if we already have max_buffer_size amount of data, fail here */ ++ if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream)) ++ return -2; ++ if (i_stream_get_data_size(zstream->istream.parent) > 0) { ++ /* Parent stream was only partially consumed. Set the stream's ++ IO as pending to avoid hangs. */ ++ i_stream_set_input_pending(&zstream->istream.istream, TRUE); ++ } ++ /* allocate enough space for the old data and the new ++ decompressed chunk. we don't know the original compressed size, ++ so just allocate the max amount of memory. */ ++ void *dest = i_stream_alloc(stream, zstream->max_uncompressed_chunk_size); ++ ret = LZ4_decompress_safe(zstream->chunk_buf->data, dest, ++ zstream->chunk_buf->used, ++ zstream->max_uncompressed_chunk_size); ++ i_assert(ret <= (int)zstream->max_uncompressed_chunk_size); ++ if (ret < 0) { ++ lz4_read_error(zstream, "corrupted lz4 chunk"); ++ stream->istream.stream_errno = EINVAL; ++ return -1; ++ } else if (ret == 0) ++ return i_stream_lz4_read(stream); ++ i_assert(ret > 0); ++ stream->pos += ret; ++ i_assert(stream->pos <= stream->buffer_size); + +- /* we are going to get next chunk after this, so reset here +- so we can reuse the chunk buf for reading next buffer prefix */ +- if (zstream->chunk_left == 0) +- buffer_set_used_size(zstream->chunk_buf, 0); ++ /* we are going to get next chunk after this, so reset here ++ so we can reuse the chunk buf for reading next buffer prefix */ ++ if (zstream->chunk_left == 0) ++ buffer_set_used_size(zstream->chunk_buf, 0); + +- return ret; ++ return ret; ++ } + } + + static void i_stream_lz4_reset(struct lz4_istream *zstream) +Index: trixie/src/lib-compression/istream-zlib.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-zlib.c ++++ trixie/src/lib-compression/istream-zlib.c +@@ -168,155 +168,157 @@ static ssize_t i_stream_zlib_read(struct + size_t size, out_size; + int ret; + +- high_offset = stream->istream.v_offset + (stream->pos - stream->skip); +- if (zstream->eof_offset == high_offset) { +- /* zlib library returned EOF. */ +- if (!zstream->gz) { +- /* deflate - ignore if there's still more data */ +- stream->istream.eof = TRUE; +- return -1; ++ for (;;) { ++ high_offset = stream->istream.v_offset + (stream->pos - stream->skip); ++ if (zstream->eof_offset == high_offset) { ++ /* zlib library returned EOF. */ ++ if (!zstream->gz) { ++ /* deflate - ignore if there's still more data */ ++ stream->istream.eof = TRUE; ++ return -1; ++ } ++ /* gz format - read the trailer */ ++ if (!zstream->trailer_read) { ++ do { ++ ret = i_stream_zlib_read_trailer(zstream); ++ } while (ret == 0 && stream->istream.blocking); ++ if (ret <= 0) ++ return ret; ++ } ++ /* See if there's another concatenated gz stream. */ ++ if (i_stream_read_eof(stream->parent)) { ++ /* EOF or error */ ++ stream->istream.stream_errno = ++ stream->parent->stream_errno; ++ stream->istream.eof = TRUE; ++ return -1; ++ } ++ /* Multiple gz streams concatenated together */ ++ zstream->starting_concatenated_output = TRUE; ++ } ++ if (zstream->starting_concatenated_output) { ++ /* make sure there actually is something in parent stream. ++ we don't want to reset the stream unless we actually see ++ some concatenated output. */ ++ ret = i_stream_read_more(stream->parent, &data, &size); ++ if (ret <= 0) { ++ if (ret == 0) ++ return 0; ++ if (stream->parent->stream_errno != 0) { ++ stream->istream.stream_errno = ++ stream->parent->stream_errno; ++ } ++ stream->istream.eof = TRUE; ++ return -1; ++ } ++ ++ /* gzip file with concatenated content */ ++ stream->cached_stream_size = UOFF_T_MAX; ++ zstream->eof_offset = UOFF_T_MAX; ++ zstream->header_read = FALSE; ++ zstream->trailer_read = FALSE; ++ zstream->crc32 = 0; ++ zstream->starting_concatenated_output = FALSE; ++ ++ (void)inflateEnd(&zstream->zs); ++ i_stream_zlib_init(zstream); + } +- /* gz format - read the trailer */ +- if (!zstream->trailer_read) { ++ ++ if (!zstream->header_read) { + do { +- ret = i_stream_zlib_read_trailer(zstream); ++ ret = i_stream_zlib_read_header(stream); + } while (ret == 0 && stream->istream.blocking); + if (ret <= 0) + return ret; ++ zstream->header_read = TRUE; + } +- /* See if there's another concatenated gz stream. */ +- if (i_stream_read_eof(stream->parent)) { +- /* EOF or error */ +- stream->istream.stream_errno = +- stream->parent->stream_errno; +- stream->istream.eof = TRUE; +- return -1; ++ ++ if (!zstream->marked) { ++ if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size)) ++ return -2; /* buffer full */ ++ } else { ++ /* try to avoid compressing, so we can quickly seek backwards */ ++ if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size)) ++ return -2; /* buffer full */ + } +- /* Multiple gz streams concatenated together */ +- zstream->starting_concatenated_output = TRUE; +- } +- if (zstream->starting_concatenated_output) { +- /* make sure there actually is something in parent stream. +- we don't want to reset the stream unless we actually see +- some concatenated output. */ +- ret = i_stream_read_more(stream->parent, &data, &size); +- if (ret <= 0) { +- if (ret == 0) +- return 0; ++ ++ if (i_stream_read_more(stream->parent, &data, &size) < 0) { + if (stream->parent->stream_errno != 0) { + stream->istream.stream_errno = + stream->parent->stream_errno; ++ } else { ++ i_assert(stream->parent->eof); ++ zlib_read_error(zstream, "unexpected EOF"); ++ stream->istream.stream_errno = EPIPE; + } +- stream->istream.eof = TRUE; + return -1; + } +- +- /* gzip file with concatenated content */ +- stream->cached_stream_size = UOFF_T_MAX; +- zstream->eof_offset = UOFF_T_MAX; +- zstream->header_read = FALSE; +- zstream->trailer_read = FALSE; +- zstream->crc32 = 0; +- zstream->starting_concatenated_output = FALSE; +- +- (void)inflateEnd(&zstream->zs); +- i_stream_zlib_init(zstream); +- } +- +- if (!zstream->header_read) { +- do { +- ret = i_stream_zlib_read_header(stream); +- } while (ret == 0 && stream->istream.blocking); +- if (ret <= 0) +- return ret; +- zstream->header_read = TRUE; +- } +- +- if (!zstream->marked) { +- if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size)) +- return -2; /* buffer full */ +- } else { +- /* try to avoid compressing, so we can quickly seek backwards */ +- if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size)) +- return -2; /* buffer full */ +- } +- +- if (i_stream_read_more(stream->parent, &data, &size) < 0) { +- if (stream->parent->stream_errno != 0) { +- stream->istream.stream_errno = +- stream->parent->stream_errno; +- } else { +- i_assert(stream->parent->eof); +- zlib_read_error(zstream, "unexpected EOF"); +- stream->istream.stream_errno = EPIPE; +- } +- return -1; +- } +- if (size == 0) { +- /* no more input */ +- i_assert(!stream->istream.blocking); +- return 0; +- } +- +- zstream->zs.next_in = (void *)data; +- zstream->zs.avail_in = size; +- +- zstream->zs.next_out = stream->w_buffer + stream->pos; +- zstream->zs.avail_out = out_size; +- ret = inflate(&zstream->zs, Z_SYNC_FLUSH); +- +- out_size -= zstream->zs.avail_out; +- /* CRC32 is only needed for GZ trailer. */ +- if (zstream->gz) +- zstream->crc32 = crc32_data_more(zstream->crc32, +- stream->w_buffer + stream->pos, +- out_size); +- stream->pos += out_size; +- +- size_t bytes_consumed = size - zstream->zs.avail_in; +- i_stream_skip(stream->parent, bytes_consumed); +- if (i_stream_get_data_size(stream->parent) > 0 && +- (bytes_consumed > 0 || out_size > 0)) { +- /* Parent stream was only partially consumed. Set the stream's +- IO as pending to avoid hangs. */ +- i_stream_set_input_pending(&stream->istream, TRUE); +- } +- +- switch (ret) { +- case Z_OK: +- break; +- case Z_NEED_DICT: +- zlib_read_error(zstream, "can't read file without dict"); +- stream->istream.stream_errno = EIO; +- return -1; +- case Z_DATA_ERROR: +- zlib_read_error(zstream, "corrupted data"); +- stream->istream.stream_errno = EINVAL; +- return -1; +- case Z_MEM_ERROR: +- i_fatal_status(FATAL_OUTOFMEM, "zlib.read(%s): Out of memory", +- i_stream_get_name(&stream->istream)); +- case Z_STREAM_END: +- zstream->eof_offset = stream->istream.v_offset + +- (stream->pos - stream->skip); +- stream->cached_stream_size = zstream->eof_offset; +- zstream->zs.avail_in = 0; +- +- if (!zstream->trailer_read) { +- /* try to read and verify the trailer, we might not +- be called again. */ +- if (i_stream_zlib_read_trailer(zstream) < 0) +- return -1; ++ if (size == 0) { ++ /* no more input */ ++ i_assert(!stream->istream.blocking); ++ return 0; ++ } ++ ++ zstream->zs.next_in = (void *)data; ++ zstream->zs.avail_in = size; ++ ++ zstream->zs.next_out = stream->w_buffer + stream->pos; ++ zstream->zs.avail_out = out_size; ++ ret = inflate(&zstream->zs, Z_SYNC_FLUSH); ++ ++ out_size -= zstream->zs.avail_out; ++ /* CRC32 is only needed for GZ trailer. */ ++ if (zstream->gz) ++ zstream->crc32 = crc32_data_more(zstream->crc32, ++ stream->w_buffer + stream->pos, ++ out_size); ++ stream->pos += out_size; ++ ++ size_t bytes_consumed = size - zstream->zs.avail_in; ++ i_stream_skip(stream->parent, bytes_consumed); ++ if (i_stream_get_data_size(stream->parent) > 0 && ++ (bytes_consumed > 0 || out_size > 0)) { ++ /* Parent stream was only partially consumed. Set the stream's ++ IO as pending to avoid hangs. */ ++ i_stream_set_input_pending(&stream->istream, TRUE); ++ } ++ ++ switch (ret) { ++ case Z_OK: ++ break; ++ case Z_NEED_DICT: ++ zlib_read_error(zstream, "can't read file without dict"); ++ stream->istream.stream_errno = EIO; ++ return -1; ++ case Z_DATA_ERROR: ++ zlib_read_error(zstream, "corrupted data"); ++ stream->istream.stream_errno = EINVAL; ++ return -1; ++ case Z_MEM_ERROR: ++ i_fatal_status(FATAL_OUTOFMEM, "zlib.read(%s): Out of memory", ++ i_stream_get_name(&stream->istream)); ++ case Z_STREAM_END: ++ zstream->eof_offset = stream->istream.v_offset + ++ (stream->pos - stream->skip); ++ stream->cached_stream_size = zstream->eof_offset; ++ zstream->zs.avail_in = 0; ++ ++ if (!zstream->trailer_read) { ++ /* try to read and verify the trailer, we might not ++ be called again. */ ++ if (i_stream_zlib_read_trailer(zstream) < 0) ++ return -1; ++ } ++ break; ++ default: ++ i_fatal("inflate() failed with %d", ret); ++ } ++ if (out_size == 0) { ++ /* read more input */ ++ return i_stream_zlib_read(stream); + } +- break; +- default: +- i_fatal("inflate() failed with %d", ret); +- } +- if (out_size == 0) { +- /* read more input */ +- return i_stream_zlib_read(stream); ++ return out_size; + } +- return out_size; + } + + static void i_stream_zlib_init(struct zlib_istream *zstream) diff --git a/patches/0001-lib-i_close_fd-Document-that-it-preserves-errno.patch b/patches/0001-lib-i_close_fd-Document-that-it-preserves-errno.patch new file mode 100644 index 0000000..21c07ce --- /dev/null +++ b/patches/0001-lib-i_close_fd-Document-that-it-preserves-errno.patch @@ -0,0 +1,25 @@ +From 5e72915a0d803154ab50958364ff4ce78ea146da Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 3 Jun 2026 18:04:42 +0300 +Subject: [PATCH 1/5] lib: i_close_fd*() - Document that it preserves errno + +--- + src/lib/fd-util.h | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/lib/fd-util.h b/src/lib/fd-util.h +index 54bdd63160..3f46892759 100644 +--- a/src/lib/fd-util.h ++++ b/src/lib/fd-util.h +@@ -17,7 +17,7 @@ void fd_close_maybe_stdio(int *fd_in, int *fd_out); + + /* Close the fd and set it to -1. This assert-crashes if fd == 0, and is a + no-op if fd == -1. Normally fd == 0 would happen only if an uninitialized +- fd is attempted to be closed, which is a bug. */ ++ fd is attempted to be closed, which is a bug. Preserves original errno. */ + void i_close_fd_path(int *fd, const char *path, const char *arg, + const char *func, const char *file, int line); + #define i_close_fd_path(fd, path) i_close_fd_path((fd), (path), #fd, __func__, __FILE__, __LINE__) +-- +2.39.5 + diff --git a/patches/0001-lib-imap-imap-match-Fix-excessive-CPU-usage-caused-b.patch b/patches/0001-lib-imap-imap-match-Fix-excessive-CPU-usage-caused-b.patch new file mode 100644 index 0000000..d492e49 --- /dev/null +++ b/patches/0001-lib-imap-imap-match-Fix-excessive-CPU-usage-caused-b.patch @@ -0,0 +1,524 @@ +From 16ff02fd5635a3b6583a276377a8b6880047d601 Mon Sep 17 00:00:00 2001 +From: Noah Meyerhans +Date: Thu, 10 Sep 2026 09:59:18 -0400 +Subject: [PATCH] lib-imap: imap-match - Fix excessive CPU usage caused by + backtracking +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Backport the NFA/non-backtracking algorithm from 2684624… while retaining the +byte-oriented matcher semantics of 2.4.1. + +Original commit message: + +Replace the recursive backtracking matcher with a Thompson-style NFA +simulation over grapheme clusters. + +Each grapheme cluster of the compressed pattern becomes one state: +LITERAL, PERCENT (consume any number of non-separator clusters) or STAR +(consume any number of clusters including separators). A virtual +ACCEPT position sits at index n_states. Simulation tracks the set of +active positions in a bitmap. Epsilon-closure (skipping a PERCENT or +STAR without consuming) is a single forward pass because the NFA is +linear: each state can only epsilon-skip to i+1. + +The resulting match is O(n_data * n_pattern) regardless of pattern +shape, with no recursion and no backtracking, so there is no way for +a malicious pattern or mailbox name to trigger exponential CPU, +excessive stack depth, or unbounded memory. + +IMAP_MATCH_YES / NO / CHILDREN / PARENT semantics are preserved: + +- YES: ACCEPT reachable after consuming all data. +- PARENT: ACCEPT was active at some point while the next data + grapheme cluster was the separator. +- CHILDREN: some active non-ACCEPT state remains after consuming all + data, and either the data ends with a separator or an + active state can still consume a separator (precomputed + as sep_accept[]). + +Inboxcase handling (case-insensitive comparison for the INBOX prefix +of data) and grapheme-cluster comparison are unchanged - the existing +match_gc logic is reused inline as literal_matches(). +pattern_compress() and pattern_is_inboxcase() are unchanged. +--- + src/lib-imap/imap-match.c | 307 ++++++++++++++++++++------------- + src/lib-imap/test-imap-match.c | 52 ++++++ + 2 files changed, 239 insertions(+), 120 deletions(-) + +diff --git a/src/lib-imap/imap-match.c b/src/lib-imap/imap-match.c +index 8603e004fb..fec5a3c242 100644 +--- a/src/lib-imap/imap-match.c ++++ b/src/lib-imap/imap-match.c +@@ -9,9 +9,44 @@ + + #include + ++/* Pattern matching is implemented as a Thompson-style NFA simulation. ++ Each byte in the compressed pattern becomes one NFA state: ++ ++ LITERAL - must consume a matching byte, with inboxcase fallback ++ PERCENT - may consume any number of non-separator bytes ++ STAR - may consume any number of bytes, including separators ++ ++ A virtual ACCEPT position sits at index n_states. Simulation tracks the ++ set of active positions in boolean arrays. Epsilon transitions (skipping ++ a PERCENT or STAR without consuming) are applied as a single forward pass, ++ because the NFA is linear: each state can only epsilon-skip to i+1. ++ ++ Complexity is O(n_data * n_pattern), regardless of wildcard count or ++ pattern shape, so there is no recursive backtracking and no way for a ++ malicious pattern or mailbox name to trigger exponential CPU usage. ++ ++ This 2.4.1 backport keeps the historical byte-oriented matching model. ++ The upstream fix operates on grapheme clusters, but backporting that would ++ require additional Unicode matching changes outside the minimal ReDoS fix. */ ++ ++enum imap_match_nfa_type { ++ IMAP_MATCH_NFA_LITERAL = 0, ++ IMAP_MATCH_NFA_PERCENT, ++ IMAP_MATCH_NFA_STAR ++}; ++ ++struct imap_match_nfa_state { ++ enum imap_match_nfa_type type; ++ bool sep_accept; ++ unsigned char ch; ++}; ++ + struct imap_match_pattern { + const char *pattern; + bool inboxcase; ++ ++ unsigned int n_states; ++ struct imap_match_nfa_state *states; + }; + + struct imap_match_glob { +@@ -23,13 +58,6 @@ struct imap_match_glob { + char patterns_data[FLEXIBLE_ARRAY_MEMBER]; + }; + +-struct imap_match_context { +- const char *inboxcase_end; +- +- char sep; +- bool inboxcase; +-}; +- + /* name of "INBOX" - must not have repeated substrings */ + static const char inbox[] = "INBOX"; + #define INBOXLEN (sizeof(inbox) - 1) +@@ -108,6 +136,48 @@ static bool pattern_is_inboxcase(const char *pattern, char separator) + return TRUE; + } + ++static void ++imap_match_compile(pool_t pool, struct imap_match_pattern *pat, char sep) ++{ ++ unsigned int i; ++ ++ pat->n_states = strlen(pat->pattern); ++ pat->states = pat->n_states == 0 ? NULL : ++ p_new(pool, struct imap_match_nfa_state, pat->n_states); ++ ++ for (i = 0; i < pat->n_states; i++) { ++ struct imap_match_nfa_state *state = &pat->states[i]; ++ unsigned char ch = (unsigned char)pat->pattern[i]; ++ ++ if (ch == '%') ++ state->type = IMAP_MATCH_NFA_PERCENT; ++ else if (ch == '*') ++ state->type = IMAP_MATCH_NFA_STAR; ++ else { ++ state->type = IMAP_MATCH_NFA_LITERAL; ++ state->ch = ch; ++ } ++ } ++ ++ for (i = pat->n_states; i > 0; i--) { ++ unsigned int idx = i - 1; ++ const struct imap_match_nfa_state *state = &pat->states[idx]; ++ bool consume_sep = ++ state->type == IMAP_MATCH_NFA_STAR || ++ (state->type == IMAP_MATCH_NFA_LITERAL && ++ state->ch == (unsigned char)sep); ++ bool eps_skippable = ++ state->type == IMAP_MATCH_NFA_PERCENT || ++ state->type == IMAP_MATCH_NFA_STAR; ++ bool next_sep_accept = ++ idx + 1 < pat->n_states ? ++ pat->states[idx + 1].sep_accept : FALSE; ++ ++ pat->states[idx].sep_accept = ++ consume_sep || (eps_skippable && next_sep_accept); ++ } ++} ++ + static struct imap_match_glob * + imap_match_init_multiple_real(pool_t pool, const char *const *patterns, + bool inboxcase, char separator) +@@ -138,7 +208,7 @@ imap_match_init_multiple_real(pool_t pool, const char *const *patterns, + glob->pool = pool; + glob->sep = separator; + +- /* copy pattern strings to our allocated memory */ ++ /* copy pattern strings to our allocated memory and compile NFAs */ + for (i = 0, pos = 0; i < patterns_count; i++) { + len = strlen(match_patterns[i].pattern) + 1; + i_assert(pos + len <= patterns_data_len); +@@ -148,6 +218,8 @@ imap_match_init_multiple_real(pool_t pool, const char *const *patterns, + match_patterns[i].pattern, len); + match_patterns[i].pattern = glob->patterns_data + pos; + pos += len; ++ ++ imap_match_compile(pool, &match_patterns[i], separator); + } + glob->patterns = match_patterns; + return glob; +@@ -172,8 +244,15 @@ imap_match_init_multiple(pool_t pool, const char *const *patterns, + + void imap_match_deinit(struct imap_match_glob **glob) + { ++ struct imap_match_pattern *p; ++ + if (glob == NULL || *glob == NULL) + return; ++ ++ for (p = (*glob)->patterns; p->pattern != NULL; p++) { ++ if (p->states != NULL) ++ p_free((*glob)->pool, p->states); ++ } + p_free((*glob)->pool, (*glob)->patterns); + p_free((*glob)->pool, *glob); + *glob = NULL; +@@ -230,148 +309,136 @@ bool imap_match_globs_equal(const struct imap_match_glob *glob1, + return p1->pattern == p2->pattern; + } + +-#define CMP_CUR_CHR(ctx, data, pattern) \ +- (*(data) == *(pattern) || \ +- (i_toupper(*(data)) == i_toupper(*(pattern)) && \ +- (data) < (ctx)->inboxcase_end)) ++static bool ++literal_matches(const struct imap_match_nfa_state *state, ++ unsigned char data_ch, bool inboxcase_pos) ++{ ++ if (state->ch == data_ch) ++ return TRUE; ++ return inboxcase_pos && ++ i_toupper(data_ch) == i_toupper(state->ch); ++} + +-static enum imap_match_result +-match_sub(struct imap_match_context *ctx, const char **data_p, +- const char **pattern_p) ++static void ++nfa_eps_close(const struct imap_match_pattern *pat, bool *bits) + { +- enum imap_match_result ret, match; + unsigned int i; +- const char *data = *data_p, *pattern = *pattern_p; +- +- /* match all non-wildcards */ +- i = 0; +- while (pattern[i] != '\0' && pattern[i] != '*' && pattern[i] != '%') { +- if (!CMP_CUR_CHR(ctx, data+i, pattern+i)) { +- if (data[i] != '\0') +- return IMAP_MATCH_NO; +- if (pattern[i] == ctx->sep) +- return IMAP_MATCH_CHILDREN; +- if (i > 0 && pattern[i-1] == ctx->sep) { +- /* data="foo/" pattern = "foo/bar/%" */ +- return IMAP_MATCH_CHILDREN; +- } +- return IMAP_MATCH_NO; +- } +- i++; +- } +- data += i; +- pattern += i; + +- if (*data == '\0' && *data_p != data && data[-1] == ctx->sep && +- *pattern != '\0') { +- /* data="/" pattern="/%..." */ +- match = IMAP_MATCH_CHILDREN; +- } else { +- match = IMAP_MATCH_NO; ++ for (i = 0; i < pat->n_states; i++) { ++ if (!bits[i]) ++ continue; ++ if (pat->states[i].type == IMAP_MATCH_NFA_PERCENT || ++ pat->states[i].type == IMAP_MATCH_NFA_STAR) ++ bits[i + 1] = TRUE; + } +- while (*pattern == '%') { +- pattern++; +- +- if (*pattern == '\0') { +- /* match, if this is the last hierarchy */ +- while (*data != '\0' && *data != ctx->sep) +- data++; +- break; +- } +- +- /* skip over this hierarchy */ +- while (*data != '\0') { +- if (CMP_CUR_CHR(ctx, data, pattern)) { +- ret = match_sub(ctx, &data, &pattern); +- if (ret == IMAP_MATCH_YES) +- break; +- +- match |= ret; +- } +- +- if (*data == ctx->sep) +- break; ++} + +- data++; +- } ++static enum imap_match_result ++imap_match_pattern_run(const struct imap_match_pattern *pat, ++ const char *data, char sep, bool inboxcase_pattern) ++{ ++ const char *inboxcase_end = data; ++ unsigned int n_bits = pat->n_states + 1; ++ enum imap_match_result result = IMAP_MATCH_NO; ++ bool parent_flag = FALSE; ++ bool data_ends_with_sep = FALSE; ++ bool *cur, *next; ++ const unsigned char *p; ++ ++ if (inboxcase_pattern && ++ strncasecmp(data, inbox, INBOXLEN) == 0 && ++ (data[INBOXLEN] == '\0' || data[INBOXLEN] == sep)) { ++ inboxcase_end += INBOXLEN; + } + +- if (*pattern != '*') { +- if (*data == '\0' && *pattern != '\0') { +- if (*pattern == ctx->sep) +- match |= IMAP_MATCH_CHILDREN; +- return match; +- } ++ cur = t_new(bool, n_bits); ++ next = t_new(bool, n_bits); ++ memset(cur, 0, n_bits * sizeof(*cur)); ++ memset(next, 0, n_bits * sizeof(*next)); + +- if (*data != '\0') { +- if (*pattern == '\0' && *data == ctx->sep) +- match |= IMAP_MATCH_PARENT; +- return match; +- } +- } ++ cur[0] = TRUE; ++ nfa_eps_close(pat, cur); + +- *data_p = data; +- *pattern_p = pattern; +- return IMAP_MATCH_YES; +-} ++ for (p = (const unsigned char *)data; *p != '\0'; p++) { ++ unsigned int i; ++ unsigned char ch = *p; ++ bool ch_is_sep = ch == (unsigned char)sep; ++ bool inboxcase_pos = (const char *)p < inboxcase_end; + +-static enum imap_match_result +-imap_match_pattern(struct imap_match_context *ctx, +- const char *data, const char *pattern) +-{ +- enum imap_match_result ret, match; ++ if (ch_is_sep && cur[pat->n_states]) ++ parent_flag = TRUE; + +- ctx->inboxcase_end = data; +- if (ctx->inboxcase && strncasecmp(data, inbox, INBOXLEN) == 0 && +- (data[INBOXLEN] == '\0' || data[INBOXLEN] == ctx->sep)) { +- /* data begins with INBOX/, use case-insensitive comparison +- for it */ +- ctx->inboxcase_end += INBOXLEN; +- } ++ memset(next, 0, n_bits * sizeof(*next)); ++ for (i = 0; i < pat->n_states; i++) { ++ const struct imap_match_nfa_state *state; + +- if (*pattern != '*') { +- /* handle the pattern up to the first '*' */ +- ret = match_sub(ctx, &data, &pattern); +- if (ret != IMAP_MATCH_YES || *pattern == '\0') +- return ret; +- } ++ if (!cur[i]) ++ continue; + +- match = IMAP_MATCH_CHILDREN; +- while (*pattern == '*') { +- pattern++; ++ state = &pat->states[i]; ++ switch (state->type) { ++ case IMAP_MATCH_NFA_LITERAL: ++ if (literal_matches(state, ch, inboxcase_pos)) ++ next[i + 1] = TRUE; ++ break; ++ case IMAP_MATCH_NFA_PERCENT: ++ if (!ch_is_sep) ++ next[i] = TRUE; ++ break; ++ case IMAP_MATCH_NFA_STAR: ++ next[i] = TRUE; ++ break; ++ } ++ } + +- if (*pattern == '\0') +- return IMAP_MATCH_YES; ++ { ++ bool *tmp = cur; ++ cur = next; ++ next = tmp; ++ } ++ nfa_eps_close(pat, cur); ++ data_ends_with_sep = ch_is_sep; ++ } + +- while (*data != '\0') { +- if (CMP_CUR_CHR(ctx, data, pattern)) { +- ret = match_sub(ctx, &data, &pattern); +- if (ret == IMAP_MATCH_YES) +- break; +- match |= ret; ++ if (cur[pat->n_states]) ++ result = IMAP_MATCH_YES; ++ else { ++ unsigned int i; ++ bool has_nonaccept = FALSE; ++ bool has_sep_accept = FALSE; ++ ++ for (i = 0; i < pat->n_states; i++) { ++ if (!cur[i]) ++ continue; ++ has_nonaccept = TRUE; ++ if (pat->states[i].sep_accept) { ++ has_sep_accept = TRUE; ++ break; + } +- +- data++; + } ++ ++ if (has_nonaccept && (data_ends_with_sep || has_sep_accept)) ++ result |= IMAP_MATCH_CHILDREN; ++ if (parent_flag) ++ result |= IMAP_MATCH_PARENT; + } + +- return *data == '\0' && *pattern == '\0' ? +- IMAP_MATCH_YES : match; ++ return result; + } + + enum imap_match_result + imap_match(struct imap_match_glob *glob, const char *data) + { +- struct imap_match_context ctx; + unsigned int i; + enum imap_match_result ret, match; + + match = IMAP_MATCH_NO; +- ctx.sep = glob->sep; + for (i = 0; glob->patterns[i].pattern != NULL; i++) { +- ctx.inboxcase = glob->patterns[i].inboxcase; +- +- ret = imap_match_pattern(&ctx, data, glob->patterns[i].pattern); ++ T_BEGIN { ++ ret = imap_match_pattern_run(&glob->patterns[i], data, ++ glob->sep, ++ glob->patterns[i].inboxcase); ++ } T_END; + if (ret == IMAP_MATCH_YES) + return IMAP_MATCH_YES; + +diff --git a/src/lib-imap/test-imap-match.c b/src/lib-imap/test-imap-match.c +index df911dae50..88459ea9c3 100644 +--- a/src/lib-imap/test-imap-match.c ++++ b/src/lib-imap/test-imap-match.c +@@ -2,6 +2,7 @@ + + #include "lib.h" + #include "imap-match.h" ++#include "str.h" + #include "test-common.h" + + struct test_imap_match { +@@ -88,6 +89,56 @@ static void test_imap_match(void) + test_end(); + } + ++static void test_imap_match_no_redos(void) ++{ ++ struct imap_match_glob *glob; ++ pool_t pool; ++ const unsigned int wildcard_count = 1000; ++ string_t *mailbox_name, *pattern; ++ unsigned int i; ++ ++ pool = pool_alloconly_create("imap match redos", 1024); ++ test_begin("imap match no redos"); ++ ++ mailbox_name = str_new(pool, 256); ++ for (i = 0; i < 255; i++) ++ str_append_c(mailbox_name, 'a'); ++ ++ pattern = str_new(pool, wildcard_count * 2 + 10); ++ for (i = 0; i < 255; i++) ++ str_append(pattern, "%a"); ++ glob = imap_match_init(pool, str_c(pattern), FALSE, '/'); ++ test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_YES); ++ ++ str_truncate(pattern, 0); ++ for (i = 0; i < wildcard_count; i++) ++ str_append(pattern, "%a"); ++ glob = imap_match_init(pool, str_c(pattern), FALSE, '/'); ++ test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_NO); ++ ++ str_append_c(pattern, 'b'); ++ glob = imap_match_init(pool, str_c(pattern), FALSE, '/'); ++ test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_NO); ++ ++ str_truncate(pattern, 0); ++ for (i = 0; i < wildcard_count; i++) ++ str_append(pattern, "*a"); ++ str_append_c(pattern, 'b'); ++ glob = imap_match_init(pool, str_c(pattern), FALSE, '/'); ++ test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_CHILDREN); ++ p_clear(pool); ++ ++ glob = imap_match_init(pool, "%a%a%a%a%ab", FALSE, '/'); ++ test_assert(imap_match(glob, "aaaaab") == IMAP_MATCH_YES); ++ p_clear(pool); ++ ++ glob = imap_match_init(pool, "%a%a%a%a%ab", FALSE, '/'); ++ test_assert(imap_match(glob, "aaaaa") == IMAP_MATCH_NO); ++ ++ pool_unref(&pool); ++ test_end(); ++} ++ + static void test_imap_match_globs_equal(void) + { + struct imap_match_glob *glob; +@@ -120,6 +171,7 @@ int main(void) + { + static void (*const test_functions[])(void) = { + test_imap_match, ++ test_imap_match_no_redos, + test_imap_match_globs_equal, + NULL + }; +-- +2.47.3 + diff --git a/patches/0001-lib-imap-urlauth-Fix-leaking-uninitialized-memory-in.patch b/patches/0001-lib-imap-urlauth-Fix-leaking-uninitialized-memory-in.patch new file mode 100644 index 0000000..3e5fbc6 --- /dev/null +++ b/patches/0001-lib-imap-urlauth-Fix-leaking-uninitialized-memory-in.patch @@ -0,0 +1,35 @@ +From 366ca92f916953c2e475ff20dd7d21fcca5a1987 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 6 May 2026 14:53:41 +0000 +Subject: [PATCH] lib-imap-urlauth: Fix leaking uninitialized memory into + client error message + +imap_urlauth_fetch_parsed() called imap_msgpart_url_open_mailbox() with +client_error_r and then, on the ret==0 (mailbox-not-found) branch, +formatted a separate uninitialized local "error" pointer with +t_strdup_printf("Invalid URLAUTH: %s", error). The %s read process +stack memory until a NUL byte and sent it to the authenticated IMAP +client inside the "* NO Failed to fetch URLAUTH ..." response. + +Broken by bb193c273e63ffa42c5c0b51ecd8860398e3beab +--- + src/lib-imap-urlauth/imap-urlauth.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/src/lib-imap-urlauth/imap-urlauth.c b/src/lib-imap-urlauth/imap-urlauth.c +index 3cdcc21bb6..21f4520a93 100644 +--- a/src/lib-imap-urlauth/imap-urlauth.c ++++ b/src/lib-imap-urlauth/imap-urlauth.c +@@ -513,7 +513,8 @@ int imap_urlauth_fetch_parsed(struct imap_urlauth_context *uctx, + } + + if ((ret = imap_msgpart_url_open_mailbox(mpurl, &box, error_code_r, +- client_error_r)) < 0) { ++ &error)) < 0) { ++ *client_error_r = t_strdup_printf("Invalid URLAUTH: %s", error); + imap_msgpart_url_free(&mpurl); + return -1; + } +-- +2.47.3 + diff --git a/patches/0001-lib-lda-mail-deliver-Autocreate-delivery-mailbox-via.patch b/patches/0001-lib-lda-mail-deliver-Autocreate-delivery-mailbox-via.patch new file mode 100644 index 0000000..b962485 --- /dev/null +++ b/patches/0001-lib-lda-mail-deliver-Autocreate-delivery-mailbox-via.patch @@ -0,0 +1,63 @@ +From 3a25b72230003ca7e133dc1117cc5e0a2f006ef3 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 11 Jun 2026 17:20:32 +0000 +Subject: [PATCH 1/2] lib-lda: mail-deliver - Autocreate delivery mailbox via + explicit mailbox_create() + +Instead of relying on MAILBOX_FLAG_AUTO_CREATE to have mailbox_open() +implicitly create the target, open it and, on MAIL_ERROR_NOTFOUND, +create it explicitly when lda_mailbox_autocreate is enabled. This makes +the creation go through the normal mailbox_create() path (including ACL +checks) and removes one of the two users of the autocreate mailbox flags. +--- + src/lib-lda/mail-deliver.c | 29 ++++++++++++++++++++++++----- + 1 file changed, 24 insertions(+), 5 deletions(-) + +diff --git a/src/lib-lda/mail-deliver.c b/src/lib-lda/mail-deliver.c +index 1001fed76b..605353bb8d 100644 +--- a/src/lib-lda/mail-deliver.c ++++ b/src/lib-lda/mail-deliver.c +@@ -244,16 +244,35 @@ int mail_deliver_save_open(struct mail_deliver_save_open_context *ctx, + return -1; + } + +- if (ctx->lda_mailbox_autocreate) +- flags |= MAILBOX_FLAG_AUTO_CREATE; +- if (ctx->lda_mailbox_autosubscribe) +- flags |= MAILBOX_FLAG_AUTO_SUBSCRIBE; + *box_r = box = mailbox_alloc_for_user(ctx->user, name, flags); + + if (mailbox_open(box) == 0) + return 0; + *error_str_r = mailbox_get_last_internal_error(box, error_r); +- return -1; ++ ++ /* Autocreate the mailbox if it does not exist yet and ++ lda_mailbox_autocreate is enabled. */ ++ if (*error_r != MAIL_ERROR_NOTFOUND || !ctx->lda_mailbox_autocreate) ++ return -1; ++ ++ if (mailbox_create(box, NULL, FALSE) < 0) { ++ enum mail_error error; ++ ++ *error_str_r = mailbox_get_last_internal_error(box, &error); ++ if (error != MAIL_ERROR_EXISTS) { ++ *error_r = error; ++ return -1; ++ } ++ /* Someone else just created it; fall through and open it. */ ++ } else if (ctx->lda_mailbox_autosubscribe) { ++ (void)mailbox_set_subscribed(box, TRUE); ++ } ++ ++ if (mailbox_open(box) < 0) { ++ *error_str_r = mailbox_get_last_internal_error(box, error_r); ++ return -1; ++ } ++ return 0; + } + + static bool mail_deliver_check_duplicate(struct mail_deliver_session *session, +-- +2.39.5 + diff --git a/patches/0001-lib-mail-Reset-charset-translation-buffer-between-MI.patch b/patches/0001-lib-mail-Reset-charset-translation-buffer-between-MI.patch new file mode 100644 index 0000000..c65010f --- /dev/null +++ b/patches/0001-lib-mail-Reset-charset-translation-buffer-between-MI.patch @@ -0,0 +1,105 @@ +From 3d300ea1f26efd7f0207c5e3efd81f644ca03bf9 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sat, 28 Feb 2026 10:27:19 +0200 +Subject: [PATCH 01/14] lib-mail: Reset charset translation buffer between MIME + parts + +If MIME part ended with an incomplete charset translation, the buffer was +kept for the next MIME part. This could have produced garbage in the next +MIME part, or a crash. + +Fixes: +Panic: file message-decoder.c: line 232 (translation_buf_decode): assertion failed: (orig_size < CHARSET_MAX_PENDING_BUF_SIZE) +--- + src/lib-mail/message-decoder.c | 4 +++ + src/lib-mail/test-message-decoder.c | 52 +++++++++++++++++++++++++++++ + 2 files changed, 56 insertions(+) + +diff --git a/src/lib-mail/message-decoder.c b/src/lib-mail/message-decoder.c +index 845b3d5e3a..4eea9994a2 100644 +--- a/src/lib-mail/message-decoder.c ++++ b/src/lib-mail/message-decoder.c +@@ -257,6 +257,10 @@ message_decode_body_init_charset(struct message_decoder_context *ctx, + if (ctx->binary_input) + return; + ++ /* If some input was left untranslated in the previous MIME part, ++ it needs to be discarded now so it won't affect the next part. */ ++ ctx->translation_size = 0; ++ + if (ctx->charset_trans != NULL && ctx->content_charset != NULL && + strcasecmp(ctx->content_charset, ctx->charset_trans_charset) == 0) { + /* already have the correct translation selected */ +diff --git a/src/lib-mail/test-message-decoder.c b/src/lib-mail/test-message-decoder.c +index edf9210cda..83c267187b 100644 +--- a/src/lib-mail/test-message-decoder.c ++++ b/src/lib-mail/test-message-decoder.c +@@ -498,6 +498,57 @@ UNICODE_REPLACEMENT_CHAR_UTF8; + test_end(); + } + ++static void test_message_decoder_charset_mime_part_change(void) ++{ ++ static const unsigned char test_message_input[] = ++"Content-Type: multipart/mixed; boundary=\"1\"\n" ++"MIME-Version: 1.0\n\n" ++"--1\n" ++"Content-Type: text/plain; charset=utf-8\n\n" ++"\xc3\n" ++"--1\n" ++"Content-Type: text/plain; charset=utf-8\n\n" ++"\xa4\n" ++"--1--\n"; ++ ++ static const char *test_message_output = ++ UNICODE_REPLACEMENT_CHAR_UTF8; ++ ++ test_begin("message decoder charset - mime part change"); ++ ++ const struct message_parser_settings parser_set = { .flags = 0, }; ++ struct message_parser_ctx *parser; ++ struct message_decoder_context *decoder; ++ struct message_part *parts; ++ struct message_block input, output; ++ struct istream *istream; ++ string_t *str_out = t_str_new(20); ++ int ret; ++ ++ pool_t pool = pool_alloconly_create("message parser", 10240); ++ istream = test_istream_create_data(test_message_input, ++ sizeof(test_message_input)-1); ++ parser = message_parser_init(pool, istream, &parser_set); ++ decoder = message_decoder_init(NULL, 0); ++ ++ while ((ret = message_parser_parse_next_block(parser, &input)) > 0) { ++ message_part_data_parse_from_header(pool, input.part, input.hdr); ++ if (message_decoder_decode_next_block(decoder, &input, &output) && ++ output.hdr == NULL && output.size > 0) ++ str_append_data(str_out, output.data, output.size); ++ } ++ ++ test_assert(ret == -1); ++ test_assert_strcmp(test_message_output, str_c(str_out)); ++ message_decoder_deinit(&decoder); ++ message_parser_deinit(&parser, &parts); ++ test_assert(istream->stream_errno == 0); ++ ++ i_stream_unref(&istream); ++ pool_unref(&pool); ++ test_end(); ++} ++ + int main(void) + { + static void (*const test_functions[])(void) = { +@@ -507,6 +558,7 @@ int main(void) + test_message_decoder_content_transfer_encoding, + test_message_decoder_invalid_content_transfer_encoding, + test_message_decoder_charset, ++ test_message_decoder_charset_mime_part_change, + NULL + }; + return test_run(test_functions); +-- +2.47.3 + diff --git a/patches/0001-lib-mail-o_stream_dot_sendv-Use-I_MIN.patch b/patches/0001-lib-mail-o_stream_dot_sendv-Use-I_MIN.patch new file mode 100644 index 0000000..9292056 --- /dev/null +++ b/patches/0001-lib-mail-o_stream_dot_sendv-Use-I_MIN.patch @@ -0,0 +1,23 @@ +From fc74e9b79107879c9f6f714c2c8becb533ba07d6 Mon Sep 17 00:00:00 2001 +From: Marco Bettini +Date: Fri, 10 Apr 2026 08:53:36 +0000 +Subject: [PATCH 1/4] lib-mail: o_stream_dot_sendv() - Use I_MIN() + +--- + src/lib-mail/ostream-dot.c | 3 +-- + 1 file changed, 1 insertion(+), 2 deletions(-) + +Index: trixie/src/lib-mail/ostream-dot.c +=================================================================== +--- trixie.orig/src/lib-mail/ostream-dot.c ++++ trixie/src/lib-mail/ostream-dot.c +@@ -198,8 +198,7 @@ o_stream_dot_sendv(struct ostream_privat + if (max_bytes == 0) + break; + i_assert(p <= pend); +- chunk = ((size_t)(p-data) >= max_bytes ? +- max_bytes : (size_t)(p - data)); ++ chunk = I_MIN((size_t)(p - data), max_bytes); + if (chunk > 0) { + iovn.iov_base = data; + iovn.iov_len = chunk; diff --git a/patches/0001-lib-mail-ostream-dot-Optimize-stream-writing.patch b/patches/0001-lib-mail-ostream-dot-Optimize-stream-writing.patch new file mode 100644 index 0000000..0f367ba --- /dev/null +++ b/patches/0001-lib-mail-ostream-dot-Optimize-stream-writing.patch @@ -0,0 +1,60 @@ +From e94a6c3cb984f9d1e5fe51714ea5015205d79fd1 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 26 Jan 2026 14:55:08 +0200 +Subject: [PATCH] lib-mail: ostream-dot - Optimize stream writing + +Use i_memcspn() to figure out how much we can skip. +--- + src/lib-mail/ostream-dot.c | 14 +++++++++++++- + 1 file changed, 13 insertions(+), 1 deletion(-) + +Index: trixie/src/lib-mail/ostream-dot.c +=================================================================== +--- trixie.orig/src/lib-mail/ostream-dot.c ++++ trixie/src/lib-mail/ostream-dot.c +@@ -108,9 +108,17 @@ o_stream_dot_sendv(struct ostream_privat + for (; p < pend && (size_t)(p-data)+2 < max_bytes; p++) { + char add = 0; + ++ size = pend - p; + switch (dstream->state) { + /* none */ +- case STREAM_STATE_NONE: ++ case STREAM_STATE_NONE: { ++ size_t maxlen = I_MIN(size, max_bytes - ((size_t)(p - data) + 2)); ++ p = CONST_PTR_OFFSET(p, i_memcspn(p, maxlen, "\r\n", 2)); ++ i_assert(p <= pend); ++ if (p == pend) { ++ p--; ++ continue; ++ } + switch (*p) { + case '\n': + dstream->state = STREAM_STATE_CRLF; +@@ -122,8 +130,10 @@ o_stream_dot_sendv(struct ostream_privat + break; + } + break; ++ } + /* got CR */ + case STREAM_STATE_CR: ++ i_assert(p < pend); + switch (*p) { + case '\r': + break; +@@ -138,6 +148,7 @@ o_stream_dot_sendv(struct ostream_privat + /* got CRLF, or the first line */ + case STREAM_STATE_INIT: + case STREAM_STATE_CRLF: ++ i_assert(p < pend); + switch (*p) { + case '\r': + dstream->state = STREAM_STATE_CR; +@@ -186,6 +197,7 @@ o_stream_dot_sendv(struct ostream_privat + + if (max_bytes == 0) + break; ++ i_assert(p <= pend); + chunk = ((size_t)(p-data) >= max_bytes ? + max_bytes : (size_t)(p - data)); + if (chunk > 0) { diff --git a/patches/0001-lib-managesieve-managesieve-parser-Fix-handling-of-l.patch b/patches/0001-lib-managesieve-managesieve-parser-Fix-handling-of-l.patch new file mode 100644 index 0000000..4a6dc8e --- /dev/null +++ b/patches/0001-lib-managesieve-managesieve-parser-Fix-handling-of-l.patch @@ -0,0 +1,30 @@ +From b949a172750084895a9b6e8317787a2229f6a8f0 Mon Sep 17 00:00:00 2001 +From: Stephan Bosch +Date: Sat, 11 Apr 2026 14:12:08 +0200 +Subject: [PATCH 1/2] lib-managesieve: managesieve-parser - Fix handling of + lone CR character + +--- + src/lib-managesieve/managesieve-parser.c | 9 +++++++++ + 1 file changed, 9 insertions(+) + +Index: trixie/pigeonhole/src/lib-managesieve/managesieve-parser.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-managesieve/managesieve-parser.c ++++ trixie/pigeonhole/src/lib-managesieve/managesieve-parser.c +@@ -441,6 +441,15 @@ static bool managesieve_parser_read_arg( + + switch (data[0]) { + case '\r': ++ if (data_size == 1) { ++ /* Wait for LF */ ++ return FALSE; ++ } ++ if (data[1] != '\n') { ++ parser->error = "CR sent without LF"; ++ return FALSE; ++ } ++ /* Fall through */ + case '\n': + /* Unexpected end of line */ + parser->eol = TRUE; diff --git a/patches/0001-lib-sieve-Add-per-user-sieve-rusage-file-for-cumulat.patch b/patches/0001-lib-sieve-Add-per-user-sieve-rusage-file-for-cumulat.patch new file mode 100644 index 0000000..d0edbc6 --- /dev/null +++ b/patches/0001-lib-sieve-Add-per-user-sieve-rusage-file-for-cumulat.patch @@ -0,0 +1,595 @@ +From da28fa5a900dc521a7f6cadf9320650dcad3cf18 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 5 May 2026 16:18:34 +0000 +Subject: [PATCH 1/7] lib-sieve: Add per-user sieve-rusage file for cumulative + CPU tracking + +Cumulative resource usage was previously stored in each compiled Sieve +binary's (.svbin) header. Because the binary file is keyed by script +name, a user could reset their CPU budget by uploading the same script +under a different name (PUTSCRIPT newname + SETACTIVE newname), +RENAMESCRIPT, or DELETESCRIPT followed by PUTSCRIPT under another +name. Each such operation produced a fresh .svbin with zero counter, +allowing the sieve_max_cpu_time limit to be bypassed indefinitely. + +Track cumulative CPU per user instead, in /sieve-rusage. +The file persists across all script renames, deletions, and re-uploads, +since it is not tied to any particular script name. Tracking is enabled +only for personal storages where the user's INBOX namespace has a +filesystem path; admin/default storages are unaffected here. + +The on-disk format is a single ASCII line: + + V1 + +CPU time is rounded up to whole seconds when persisted; sub-second +precision is preserved only for the in-memory accumulation within a +single execution. The file is updated by writing a new copy via +file_dotlock_open_mode() and replacing the original via +file_dotlock_replace(). Concurrent writers are serialized through the +dotlock; readers do not need a lock because the rename is atomic. + +The .svbin header still carries its original resource_usage struct as +the active fallback when no per-user file is available; a follow-up +commit drops that path. +--- + src/lib-sieve/Makefile.am | 2 + + src/lib-sieve/sieve-binary-file.c | 44 +++++ + src/lib-sieve/sieve-binary.c | 35 ++++ + src/lib-sieve/sieve-binary.h | 5 + + src/lib-sieve/sieve-rusage.c | 266 ++++++++++++++++++++++++++ + src/lib-sieve/sieve-rusage.h | 32 ++++ + src/lib-sieve/sieve-storage-private.h | 11 ++ + src/lib-sieve/sieve-storage.c | 2 + + src/lib-sieve/sieve.c | 5 + + 9 files changed, 402 insertions(+) + create mode 100644 src/lib-sieve/sieve-rusage.c + create mode 100644 src/lib-sieve/sieve-rusage.h + +Index: trixie/pigeonhole/src/lib-sieve/Makefile.am +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/Makefile.am ++++ trixie/pigeonhole/src/lib-sieve/Makefile.am +@@ -109,6 +109,7 @@ libdovecot_sieve_la_SOURCES = \ + sieve-ast.c \ + sieve-binary.c \ + sieve-binary-file.c \ ++ sieve-rusage.c \ + sieve-binary-code.c \ + sieve-binary-debug.c \ + sieve-parser.c \ +@@ -158,6 +159,7 @@ headers = \ + sieve-ast.h \ + sieve-binary.h \ + sieve-binary-private.h \ ++ sieve-rusage.h \ + sieve-parser.h \ + sieve-address.h \ + sieve-validator.h \ +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-file.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c +@@ -18,6 +18,9 @@ + #include "sieve-extensions.h" + #include "sieve-code.h" + #include "sieve-script.h" ++#include "sieve-script-private.h" ++#include "sieve-storage-private.h" ++#include "sieve-rusage.h" + + #include "sieve-binary-private.h" + +@@ -1008,15 +1011,56 @@ sieve_binary_file_do_update_resource_usa + return ret; + } + ++static int ++sieve_binary_storage_update_resource_usage(struct sieve_binary *sbin, ++ struct sieve_storage *storage, ++ enum sieve_error *error_code_r) ++{ ++ struct sieve_resource_usage delta; ++ struct sieve_resource_usage total; ++ uint32_t flags; ++ ++ delta = sbin->rusage; ++ sieve_binary_get_resource_usage(sbin, &total); ++ flags = sbin->header.flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT; ++ ++ if (!HAS_ALL_BITS(flags, SIEVE_BINARY_FLAG_RESOURCE_LIMIT) && ++ !sieve_resource_usage_is_high(sbin->svinst, &total)) { ++ /* Nothing meaningful to persist */ ++ sieve_resource_usage_init(&sbin->rusage); ++ sbin->rusage_updated = FALSE; ++ return 0; ++ } ++ ++ if (sieve_rusage_storage_add(storage, &delta, flags) < 0) { ++ *error_code_r = SIEVE_ERROR_TEMP_FAILURE; ++ return -1; ++ } ++ ++ sieve_resource_usage_init(&sbin->rusage); ++ sbin->rusage_updated = FALSE; ++ return 0; ++} ++ + int sieve_binary_file_update_resource_usage(struct sieve_binary *sbin, + enum sieve_error *error_code_r) + { ++ struct sieve_storage *storage = NULL; + int fd, ret = 0; + + sieve_error_args_init(&error_code_r, NULL); + + sieve_binary_file_close(&sbin->file); + ++ if (sbin->script != NULL && sbin->script->storage != NULL && ++ sbin->script->storage->rusage_path != NULL) ++ storage = sbin->script->storage; ++ ++ if (storage != NULL) { ++ return sieve_binary_storage_update_resource_usage( ++ sbin, storage, error_code_r); ++ } ++ + if (sbin->path == NULL) + return 0; + if (sbin->header.version_major != SIEVE_BINARY_VERSION_MAJOR) { +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary.c +@@ -9,6 +9,7 @@ + #include "hash.h" + #include "array.h" + #include "ostream.h" ++#include "ioloop.h" + #include "eacces-error.h" + #include "safe-mkstemp.h" + +@@ -16,6 +17,9 @@ + #include "sieve-extensions.h" + #include "sieve-code.h" + #include "sieve-script.h" ++#include "sieve-script-private.h" ++#include "sieve-storage-private.h" ++#include "sieve-rusage.h" + + #include "sieve-binary-private.h" + +@@ -190,6 +194,37 @@ void sieve_binary_close(struct sieve_bin + * Resource usage + */ + ++static struct sieve_storage * ++sieve_binary_get_user_storage(struct sieve_binary *sbin) ++{ ++ if (sbin->script == NULL) ++ return NULL; ++ if (sbin->script->storage == NULL) ++ return NULL; ++ if (sbin->script->storage->rusage_path == NULL) ++ return NULL; ++ return sbin->script->storage; ++} ++ ++void sieve_binary_apply_persisted_rusage(struct sieve_binary *sbin) ++{ ++ struct sieve_storage *storage = sieve_binary_get_user_storage(sbin); ++ struct sieve_resource_usage rusage; ++ struct sieve_binary_header *header = &sbin->header; ++ uint32_t flags = 0; ++ ++ if (storage == NULL) ++ return; ++ if (sieve_rusage_storage_load(storage, &rusage, &flags) <= 0) ++ return; ++ ++ header->resource_usage.cpu_time_msecs = rusage.cpu_time_msecs; ++ header->resource_usage.update_time = ioloop_time; ++ header->flags |= (flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT); ++ sieve_resource_usage_init(&sbin->rusage); ++ sbin->rusage_updated = FALSE; ++} ++ + void sieve_binary_get_resource_usage(struct sieve_binary *sbin, + struct sieve_resource_usage *rusage_r) + { +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary.h +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary.h ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary.h +@@ -37,6 +37,11 @@ bool sieve_binary_record_resource_usage( + ATTR_NULL(1); + void sieve_binary_set_resource_usage(struct sieve_binary *sbin, + const struct sieve_resource_usage *rusage); ++ ++/* Apply persisted per-user rusage and limit flag from sieve-rusage file ++ into the binary header. No-op when the storage has no per-user file. */ ++void sieve_binary_apply_persisted_rusage(struct sieve_binary *sbin); ++ + /* + * Accessors + */ +Index: trixie/pigeonhole/src/lib-sieve/sieve-rusage.c +=================================================================== +--- /dev/null ++++ trixie/pigeonhole/src/lib-sieve/sieve-rusage.c +@@ -0,0 +1,266 @@ ++/* Copyright (c) 2026 Pigeonhole authors, see the included COPYING file ++ */ ++ ++#include "lib.h" ++#include "str.h" ++#include "strnum.h" ++#include "ioloop.h" ++#include "eacces-error.h" ++#include "file-dotlock.h" ++#include "mail-namespace.h" ++#include "mail-storage.h" ++#include "mailbox-list.h" ++ ++#include "sieve-common.h" ++#include "sieve-settings.h" ++#include "sieve-storage-private.h" ++#include "sieve-rusage.h" ++ ++#include ++#include ++#include ++#include ++ ++#define SIEVE_RUSAGE_FILENAME "sieve-rusage" ++#define SIEVE_RUSAGE_VERSION_TAG "V1" ++#define SIEVE_RUSAGE_LOCK_TIMEOUT 10 ++#define SIEVE_RUSAGE_LOCK_STALE_TIMEOUT 60 ++ ++/* Maximum size of the on-disk file. Single-line ASCII format with four ++ space-separated tokens; anything larger is treated as corrupt. */ ++#define SIEVE_RUSAGE_MAX_FILE_SIZE 128 ++ ++struct sieve_rusage_record { ++ uint32_t flags; ++ uint32_t cpu_time_msecs; ++ time_t update_time; ++}; ++ ++void sieve_rusage_storage_init(struct sieve_storage *storage, ++ struct mail_user *user) ++{ ++ struct mail_namespace *ns; ++ const char *root; ++ ++ if (!storage->is_personal || user == NULL) ++ return; ++ ++ ns = mail_namespace_find_inbox(user->namespaces); ++ if (ns == NULL) ++ return; ++ ++ if (!mailbox_list_get_root_path(ns->list, MAILBOX_LIST_PATH_TYPE_DIR, ++ &root) || root == NULL) ++ return; ++ ++ storage->user = user; ++ storage->inbox_list = ns->list; ++ storage->rusage_path = p_strconcat(storage->pool, root, "/", ++ SIEVE_RUSAGE_FILENAME, NULL); ++} ++ ++static bool ++sieve_rusage_parse(const char *line, struct sieve_rusage_record *rec) ++{ ++ const char *const *tokens; ++ uint32_t cpu_secs; ++ bool ok = FALSE; ++ ++ T_BEGIN { ++ tokens = t_strsplit_spaces(line, " \t"); ++ if (tokens[0] == NULL || ++ strcmp(tokens[0], SIEVE_RUSAGE_VERSION_TAG) != 0) ++ break; ++ if (tokens[1] == NULL || tokens[2] == NULL || ++ tokens[3] == NULL || tokens[4] != NULL) ++ break; ++ if (str_to_uint32(tokens[1], &rec->flags) < 0) ++ break; ++ if (str_to_uint32(tokens[2], &cpu_secs) < 0) ++ break; ++ if (str_to_time(tokens[3], &rec->update_time) < 0) ++ break; ++ if (cpu_secs > UINT32_MAX / 1000) ++ rec->cpu_time_msecs = UINT32_MAX; ++ else ++ rec->cpu_time_msecs = cpu_secs * 1000; ++ ok = TRUE; ++ } T_END; ++ ++ return ok; ++} ++ ++static int ++sieve_rusage_read(struct sieve_storage *storage, ++ struct sieve_rusage_record *rec_r) ++{ ++ char buf[SIEVE_RUSAGE_MAX_FILE_SIZE + 1]; ++ char *nl; ++ ssize_t ret; ++ int fd; ++ ++ i_zero(rec_r); ++ ++ fd = open(storage->rusage_path, O_RDONLY); ++ if (fd < 0) { ++ if (errno == ENOENT) ++ return 0; ++ e_error(storage->event, "open(%s) failed: %m", ++ storage->rusage_path); ++ return -1; ++ } ++ ++ ret = read(fd, buf, sizeof(buf) - 1); ++ i_close_fd(&fd); ++ if (ret < 0) { ++ e_error(storage->event, "read(%s) failed: %m", ++ storage->rusage_path); ++ return -1; ++ } ++ if (ret == 0) ++ return 0; ++ if (ret >= (ssize_t)(sizeof(buf) - 1)) { ++ e_warning(storage->event, "%s: file too large; resetting", ++ storage->rusage_path); ++ return 0; ++ } ++ buf[ret] = '\0'; ++ ++ nl = strchr(buf, '\n'); ++ if (nl != NULL) ++ *nl = '\0'; ++ ++ if (!sieve_rusage_parse(buf, rec_r)) { ++ e_warning(storage->event, "%s: malformed content; resetting", ++ storage->rusage_path); ++ i_zero(rec_r); ++ } ++ return 0; ++} ++ ++static void ++sieve_rusage_dotlock_settings(struct dotlock_settings *set_r) ++{ ++ i_zero(set_r); ++ set_r->timeout = SIEVE_RUSAGE_LOCK_TIMEOUT; ++ set_r->stale_timeout = SIEVE_RUSAGE_LOCK_STALE_TIMEOUT; ++ set_r->use_excl_lock = TRUE; ++} ++ ++int sieve_rusage_storage_load(struct sieve_storage *storage, ++ struct sieve_resource_usage *rusage_r, ++ uint32_t *flags_r) ++{ ++ struct sieve_rusage_record rec; ++ unsigned int timeout; ++ ++ sieve_resource_usage_init(rusage_r); ++ *flags_r = 0; ++ ++ if (storage->rusage_path == NULL) ++ return 0; ++ ++ if (sieve_rusage_read(storage, &rec) < 0) ++ return -1; ++ ++ timeout = storage->svinst->set->resource_usage_timeout; ++ if (rec.update_time != 0 && ++ (ioloop_time - rec.update_time) > (time_t)timeout) { ++ /* decayed */ ++ return 1; ++ } ++ ++ rusage_r->cpu_time_msecs = rec.cpu_time_msecs; ++ *flags_r = rec.flags; ++ return 1; ++} ++ ++int sieve_rusage_storage_add(struct sieve_storage *storage, ++ const struct sieve_resource_usage *delta_rusage, ++ uint32_t flags_to_set) ++{ ++ struct sieve_rusage_record rec; ++ struct dotlock_settings dlset; ++ struct dotlock *dotlock; ++ unsigned int timeout; ++ uint32_t old_cpu, cpu_secs; ++ string_t *out; ++ ssize_t wret; ++ int fd; ++ ++ if (storage->rusage_path == NULL) ++ return 0; ++ ++ sieve_rusage_dotlock_settings(&dlset); ++ ++ /* The namespace root may not exist yet on the very first update for ++ a user (e.g. LDA refused execution before any mail was delivered). ++ Create it on demand so file_dotlock_open does not fail with ENOENT. */ ++ if (storage->inbox_list != NULL && ++ mailbox_list_mkdir_root(storage->inbox_list, NULL, ++ MAILBOX_LIST_PATH_TYPE_DIR) < 0) ++ return -1; ++ ++ fd = file_dotlock_open_mode(&dlset, storage->rusage_path, 0, ++ 0600, (uid_t)-1, (gid_t)-1, &dotlock); ++ if (fd == -1) { ++ if (errno == EACCES) { ++ e_error(storage->event, "%s", ++ eacces_error_get_creating("file_dotlock_open", ++ storage->rusage_path)); ++ } else { ++ e_error(storage->event, ++ "file_dotlock_open(%s) failed: %m", ++ storage->rusage_path); ++ } ++ return -1; ++ } ++ ++ if (sieve_rusage_read(storage, &rec) < 0) { ++ file_dotlock_delete(&dotlock); ++ return -1; ++ } ++ ++ timeout = storage->svinst->set->resource_usage_timeout; ++ if (rec.update_time != 0 && ++ (ioloop_time - rec.update_time) > (time_t)timeout) { ++ /* decayed: discard previous values */ ++ rec.cpu_time_msecs = 0; ++ rec.flags = 0; ++ } ++ ++ old_cpu = rec.cpu_time_msecs; ++ if ((UINT32_MAX - old_cpu) < delta_rusage->cpu_time_msecs) ++ rec.cpu_time_msecs = UINT32_MAX; ++ else ++ rec.cpu_time_msecs = old_cpu + delta_rusage->cpu_time_msecs; ++ rec.flags |= flags_to_set; ++ rec.update_time = ioloop_time; ++ ++ /* Round CPU time up to whole seconds for persistence. */ ++ if (rec.cpu_time_msecs >= UINT32_MAX - 999) ++ cpu_secs = UINT32_MAX / 1000; ++ else ++ cpu_secs = (rec.cpu_time_msecs + 999) / 1000; ++ ++ out = t_str_new(64); ++ str_printfa(out, "%s %u %u %lld\n", SIEVE_RUSAGE_VERSION_TAG, ++ rec.flags, cpu_secs, (long long)rec.update_time); ++ ++ wret = write(fd, str_data(out), str_len(out)); ++ if (wret != (ssize_t)str_len(out)) { ++ e_error(storage->event, ++ "write(%s) failed: %m (wrote %zd/%zu)", ++ storage->rusage_path, wret, str_len(out)); ++ file_dotlock_delete(&dotlock); ++ return -1; ++ } ++ ++ if (file_dotlock_replace(&dotlock, 0) < 0) { ++ e_error(storage->event, ++ "file_dotlock_replace(%s) failed: %m", ++ storage->rusage_path); ++ return -1; ++ } ++ return 1; ++} +Index: trixie/pigeonhole/src/lib-sieve/sieve-rusage.h +=================================================================== +--- /dev/null ++++ trixie/pigeonhole/src/lib-sieve/sieve-rusage.h +@@ -0,0 +1,32 @@ ++#ifndef SIEVE_RUSAGE_H ++#define SIEVE_RUSAGE_H ++ ++#include "sieve-common.h" ++ ++struct sieve_storage; ++struct mail_user; ++ ++/* Per-user resource usage file lives at /sieve-rusage. ++ It survives script renames, deletions, and re-uploads under a different ++ name, preventing CPU-budget bypass. Tracking is enabled only for personal ++ user-controlled storage. */ ++ ++void sieve_rusage_storage_init(struct sieve_storage *storage, ++ struct mail_user *user); ++ ++/* Load persisted resource usage from the per-user file. Returns 1 on success, ++ 0 if the storage has no rusage file (no INBOX namespace path), -1 on error. ++ Applies the resource_usage_timeout decay. flags_r receives sieve binary ++ header flags (RESOURCE_LIMIT) that were persisted. */ ++int sieve_rusage_storage_load(struct sieve_storage *storage, ++ struct sieve_resource_usage *rusage_r, ++ uint32_t *flags_r); ++ ++/* Atomically add delta_rusage to and OR flags_to_set into the persisted ++ per-user rusage file. Read-modify-write under fcntl WRLCK. Returns 1 on ++ success, 0 if no rusage file is configured, -1 on error. */ ++int sieve_rusage_storage_add(struct sieve_storage *storage, ++ const struct sieve_resource_usage *delta_rusage, ++ uint32_t flags_to_set); ++ ++#endif +Index: trixie/pigeonhole/src/lib-sieve/sieve-storage-private.h +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-storage-private.h ++++ trixie/pigeonhole/src/lib-sieve/sieve-storage-private.h +@@ -118,6 +118,17 @@ struct sieve_storage { + + struct mail_namespace *sync_inbox_ns; + ++ /* Per-user resource usage tracking. Set when storage is personal and ++ the user's INBOX namespace has a filesystem path. NULL otherwise. ++ inbox_list is kept so the namespace root directory can be created ++ on demand before the rusage file is updated; LDA may close the ++ sieve binary (and therefore persist rusage) before any mail has ++ been delivered, in which case the namespace root may not exist ++ yet. */ ++ struct mail_user *user; ++ struct mailbox_list *inbox_list; ++ const char *rusage_path; ++ + enum sieve_storage_flags flags; + + bool allows_synchronization:1; +Index: trixie/pigeonhole/src/lib-sieve/sieve-storage.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-storage.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-storage.c +@@ -17,6 +17,7 @@ + + #include "sieve-script-private.h" + #include "sieve-storage-private.h" ++#include "sieve-rusage.h" + + #include + #include +@@ -777,6 +778,7 @@ int sieve_storage_create_personal(struct + if (ret == 0) { + i_assert(storage->is_personal); + (void)sieve_storage_sync_init(storage, user); ++ sieve_rusage_storage_init(storage, user); + } else if (*error_code_r != SIEVE_ERROR_TEMP_FAILURE && + (flags & SIEVE_STORAGE_FLAG_SYNCHRONIZING) == 0 && + (flags & SIEVE_STORAGE_FLAG_READWRITE) == 0) { +Index: trixie/pigeonhole/src/lib-sieve/sieve.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve.c ++++ trixie/pigeonhole/src/lib-sieve/sieve.c +@@ -30,6 +30,7 @@ + #include "sieve-generator.h" + #include "sieve-interpreter.h" + #include "sieve-binary-dumper.h" ++#include "sieve-rusage.h" + + #include "sieve.h" + #include "sieve-common.h" +@@ -444,6 +445,8 @@ sieve_open_script_real(struct sieve_scri + + /* Try to open the matching binary */ + if (sieve_script_binary_load(script, &sbin, error_code_r) == 0) { ++ /* Per-user rusage file (if any) overrides binary header */ ++ sieve_binary_apply_persisted_rusage(sbin); + sieve_binary_get_resource_usage(sbin, &rusage); + + /* Ok, it exists; now let's see if it is up to date */ +@@ -474,6 +477,8 @@ sieve_open_script_real(struct sieve_scri + sieve_script_label(script)); + + sieve_binary_set_resource_usage(sbin, &rusage); ++ /* Per-user rusage file (if any) overrides any carry-forward */ ++ sieve_binary_apply_persisted_rusage(sbin); + } + + /* Check whether binary can be executed. */ diff --git a/patches/0001-lib-sieve-actions-Split-mailbox-creation-into-sieve_.patch b/patches/0001-lib-sieve-actions-Split-mailbox-creation-into-sieve_.patch new file mode 100644 index 0000000..184b384 --- /dev/null +++ b/patches/0001-lib-sieve-actions-Split-mailbox-creation-into-sieve_.patch @@ -0,0 +1,124 @@ +From 4010e318939b557b0cdf7da3ae0f320d408aea65 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 11 Jun 2026 19:31:45 +0000 +Subject: [PATCH 1/2] lib-sieve: actions - Split mailbox creation into + sieve_act_store_create_mailbox() + +Pure refactoring with no functional change: move the mailbox_create() / +subscribe / reopen sequence out of the "mailbox" extension's :create side +effect (seff_mailbox_create_pre_execute) into a new reusable +sieve_act_store_create_mailbox() helper. +--- + .../plugins/mailbox/tag-mailbox-create.c | 32 +--------------- + src/lib-sieve/sieve-actions.c | 38 +++++++++++++++++++ + src/lib-sieve/sieve-actions.h | 5 +++ + 3 files changed, 45 insertions(+), 30 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/plugins/mailbox/tag-mailbox-create.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/plugins/mailbox/tag-mailbox-create.c ++++ trixie/pigeonhole/src/lib-sieve/plugins/mailbox/tag-mailbox-create.c +@@ -150,36 +150,8 @@ seff_mailbox_create_pre_execute( + return SIEVE_EXEC_FAILURE; + } + +- trans->error = NULL; +- trans->error_code = MAIL_ERROR_NONE; +- +- /* Create mailbox */ +- if (mailbox_create(box, NULL, FALSE) < 0) { +- sieve_act_store_get_storage_error(aenv, trans); +- if (trans->error_code == MAIL_ERROR_EXISTS) { +- trans->error = NULL; +- trans->error_code = MAIL_ERROR_NONE; +- } else { +- return (trans->error_code == MAIL_ERROR_TEMP ? +- SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE); +- } +- } +- +- /* Subscribe to it if necessary */ +- if (eenv->scriptenv->mailbox_autosubscribe) { +- (void)mailbox_list_set_subscribed( +- mailbox_get_namespace(box)->list, +- mailbox_get_name(box), TRUE); +- } +- +- /* Try opening again */ +- if (mailbox_open(box) < 0) { +- /* Failed definitively */ +- sieve_act_store_get_storage_error(aenv, trans); +- return (trans->error_code == MAIL_ERROR_TEMP ? +- SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE); +- } +- return SIEVE_EXEC_OK; ++ /* Create the mailbox */ ++ return sieve_act_store_create_mailbox(aenv, trans); + } + + +Index: trixie/pigeonhole/src/lib-sieve/sieve-actions.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-actions.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-actions.c +@@ -372,6 +372,44 @@ void sieve_act_store_get_storage_error(c + &trans->error_code)); + } + ++int sieve_act_store_create_mailbox(const struct sieve_action_exec_env *aenv, ++ struct act_store_transaction *trans) ++{ ++ const struct sieve_execute_env *eenv = aenv->exec_env; ++ struct mailbox *box = trans->box; ++ ++ trans->error = NULL; ++ trans->error_code = MAIL_ERROR_NONE; ++ ++ /* Create mailbox */ ++ if (mailbox_create(box, NULL, FALSE) < 0) { ++ sieve_act_store_get_storage_error(aenv, trans); ++ if (trans->error_code == MAIL_ERROR_EXISTS) { ++ trans->error = NULL; ++ trans->error_code = MAIL_ERROR_NONE; ++ } else { ++ return (trans->error_code == MAIL_ERROR_TEMP ? ++ SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE); ++ } ++ } ++ ++ /* Subscribe to it if necessary */ ++ if (eenv->scriptenv->mailbox_autosubscribe) { ++ (void)mailbox_list_set_subscribed( ++ mailbox_get_namespace(box)->list, ++ mailbox_get_name(box), TRUE); ++ } ++ ++ /* Try opening again */ ++ if (mailbox_open(box) < 0) { ++ /* Failed definitively */ ++ sieve_act_store_get_storage_error(aenv, trans); ++ return (trans->error_code == MAIL_ERROR_TEMP ? ++ SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE); ++ } ++ return SIEVE_EXEC_OK; ++} ++ + static bool + act_store_mailbox_alloc(const struct sieve_action_exec_env *aenv, + const char *mailbox, struct mailbox **box_r, +Index: trixie/pigeonhole/src/lib-sieve/sieve-actions.h +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-actions.h ++++ trixie/pigeonhole/src/lib-sieve/sieve-actions.h +@@ -246,6 +246,11 @@ void sieve_act_store_add_flags(const str + void sieve_act_store_get_storage_error(const struct sieve_action_exec_env *aenv, + struct act_store_transaction *trans); + ++/* Create the target mailbox of the store transaction (and subscribe to it ++ when configured). */ ++int sieve_act_store_create_mailbox(const struct sieve_action_exec_env *aenv, ++ struct act_store_transaction *trans); ++ + /* + * Redirect action + */ diff --git a/patches/0001-lib-sieve-sieve-binary-code-Fix-single-byte-oob-stac.patch b/patches/0001-lib-sieve-sieve-binary-code-Fix-single-byte-oob-stac.patch new file mode 100644 index 0000000..b14ea91 --- /dev/null +++ b/patches/0001-lib-sieve-sieve-binary-code-Fix-single-byte-oob-stac.patch @@ -0,0 +1,41 @@ +From 3f0af33abcc9911b586be9b5adc0afb3e9f18d25 Mon Sep 17 00:00:00 2001 +From: Stephan Bosch +Date: Fri, 10 Apr 2026 02:27:17 +0200 +Subject: [PATCH] lib-sieve: sieve-binary-code - Fix single byte oob stack + buffer write in sieve_binary_emit_integer() + +--- + src/lib-sieve/sieve-binary-code.c | 2 +- + tests/test-size.svtest | 9 +++++++++ + 2 files changed, 10 insertions(+), 1 deletion(-) + +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-code.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-code.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-code.c +@@ -124,7 +124,7 @@ sieve_size_t sieve_binary_emit_integer(s + sieve_number_t integer) + { + sieve_size_t address = _sieve_binary_block_get_size(sblock); +- uint8_t buffer[sizeof(sieve_number_t) + 1]; ++ uint8_t buffer[sizeof(sieve_number_t) * 8 / 7 + 1]; + int bufpos = sizeof(buffer) - 1; + + /* Encode last byte [0xxxxxxx]; msb == 0 marks the last byte */ +Index: trixie/pigeonhole/tests/test-size.svtest +=================================================================== +--- trixie.orig/pigeonhole/tests/test-size.svtest ++++ trixie/pigeonhole/tests/test-size.svtest +@@ -72,3 +72,12 @@ test "Exact size" { + } + } + ++/* ++ * TEST: Extreme size ++ */ ++ ++test "Extreme size" { ++ if size :over 18446744073709551615 { ++ test_fail "size :over matched extreme size"; ++ } ++} diff --git a/patches/0001-lib-sieve-sieve-storage-Add-is_personal-bit.patch b/patches/0001-lib-sieve-sieve-storage-Add-is_personal-bit.patch new file mode 100644 index 0000000..24f1810 --- /dev/null +++ b/patches/0001-lib-sieve-sieve-storage-Add-is_personal-bit.patch @@ -0,0 +1,72 @@ +From d82c2e624e4a9c364a6fa58fad0938ae5578660c Mon Sep 17 00:00:00 2001 +From: Stephan Bosch +Date: Tue, 25 Nov 2025 05:07:16 +0100 +Subject: [PATCH] lib-sieve: sieve-storage - Add is_personal bit + +Simpler than evaluating the type and personal storage is created only through +sieve_storage_create_personal(). This mimics the bit for default storage. +--- + src/lib-sieve/sieve-storage-private.h | 1 + + src/lib-sieve/sieve-storage.c | 3 ++- + src/lib-sieve/storage/file/sieve-file-storage.c | 6 ++---- + 3 files changed, 5 insertions(+), 5 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/sieve-storage-private.h +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-storage-private.h ++++ trixie/pigeonhole/src/lib-sieve/sieve-storage-private.h +@@ -121,6 +121,7 @@ struct sieve_storage { + enum sieve_storage_flags flags; + + bool allows_synchronization:1; ++ bool is_personal:1; + bool is_default:1; + }; + +Index: trixie/pigeonhole/src/lib-sieve/sieve-storage.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-storage.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-storage.c +@@ -773,6 +773,7 @@ int sieve_storage_create_personal(struct + &storage, error_code_r, NULL); + if (ret == 0) { + (void)sieve_storage_sync_init(storage, user); ++ storage->is_personal = TRUE; + } else if (*error_code_r != SIEVE_ERROR_TEMP_FAILURE && + (flags & SIEVE_STORAGE_FLAG_SYNCHRONIZING) == 0 && + (flags & SIEVE_STORAGE_FLAG_READWRITE) == 0) { +@@ -1017,7 +1018,7 @@ sieve_storage_get_default_script(struct + + if (*error_code_r != SIEVE_ERROR_NOT_FOUND || + (storage->flags & SIEVE_STORAGE_FLAG_SYNCHRONIZING) != 0 || +- !sieve_storage_is_personal(storage)) ++ !storage->is_personal) + return -1; + + /* Not found; if this name maps to the default script, +Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c ++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c +@@ -459,11 +459,9 @@ sieve_file_storage_init_from_settings( + + /* Stat storage directory */ + +- bool is_personal = sieve_storage_is_personal(storage); +- + if (storage_path != NULL && *storage_path != '\0') { + if (sieve_file_storage_stat(fstorage, storage_path) < 0) { +- if (!is_personal || ++ if (!storage->is_personal || + storage->error_code != SIEVE_ERROR_NOT_FOUND) + return -1; + if ((storage->flags & SIEVE_STORAGE_FLAG_READWRITE) == 0) { +@@ -511,7 +509,7 @@ sieve_file_storage_init_from_settings( + } + + if ((active_path == NULL || *active_path == '\0') && +- (is_personal || ++ (storage->is_personal || + (storage->flags & SIEVE_STORAGE_FLAG_READWRITE) != 0)) { + e_debug(storage->event, + "Active script path is unconfigured; " diff --git a/patches/0001-lib-sieve-sieve-storage-Fix-panic-caused-by-flawed-d.patch b/patches/0001-lib-sieve-sieve-storage-Fix-panic-caused-by-flawed-d.patch new file mode 100644 index 0000000..7c3c042 --- /dev/null +++ b/patches/0001-lib-sieve-sieve-storage-Fix-panic-caused-by-flawed-d.patch @@ -0,0 +1,53 @@ +From 4d0cbfe6260d567ebc34a747c29bb4e140124d93 Mon Sep 17 00:00:00 2001 +From: Stephan Bosch +Date: Wed, 4 Mar 2026 21:53:36 +0100 +Subject: [PATCH] lib-sieve: sieve-storage - Fix panic caused by flawed + detection of personal storage type + +Recent commit d82c2e624e4a9c364a6fa58fad0938ae5578660c added the is_personal +flag, but it is assigned too late. Fix this issue by assigning this flag in the +earliest allocation function. + +This issue commonly surfaces when people forget to set an appropriate type for a +script storage definition, in which case the "personal" type is the default. + +Panic was: +Panic: file sieve-file-storage-active.c: line 33 +(sieve_file_storage_active_read_link): assertion failed: (fstorage->active_path != NULL) +--- + src/lib-sieve/sieve-storage.c | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/sieve-storage.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-storage.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-storage.c +@@ -232,6 +232,9 @@ sieve_storage_alloc_from_class(struct si + storage->pool, "auto:", storage->type, NULL); + } + ++ if (strcasecmp(script_type, SIEVE_STORAGE_TYPE_PERSONAL) == 0) ++ storage->is_personal = TRUE; ++ + storage->event = event; + event_ref(event); + +@@ -772,8 +775,8 @@ int sieve_storage_create_personal(struct + SIEVE_STORAGE_TYPE_PERSONAL, flags, + &storage, error_code_r, NULL); + if (ret == 0) { ++ i_assert(storage->is_personal); + (void)sieve_storage_sync_init(storage, user); +- storage->is_personal = TRUE; + } else if (*error_code_r != SIEVE_ERROR_TEMP_FAILURE && + (flags & SIEVE_STORAGE_FLAG_SYNCHRONIZING) == 0 && + (flags & SIEVE_STORAGE_FLAG_READWRITE) == 0) { +@@ -1894,7 +1897,7 @@ bool sieve_storage_is_default(const stru + + bool sieve_storage_is_personal(struct sieve_storage *storage) + { +- return (strcasecmp(storage->type, SIEVE_STORAGE_TYPE_PERSONAL) == 0); ++ return storage->is_personal; + } + + /* diff --git a/patches/0001-lib-sieve-storage-file-Refuse-symlinks-escaping-pers.patch b/patches/0001-lib-sieve-storage-file-Refuse-symlinks-escaping-pers.patch new file mode 100644 index 0000000..5c4bf78 --- /dev/null +++ b/patches/0001-lib-sieve-storage-file-Refuse-symlinks-escaping-pers.patch @@ -0,0 +1,267 @@ +From abfb0c1fe180f0edd5527c05cb7dd97840a31ea1 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 4 May 2026 13:09:37 +0000 +Subject: [PATCH 1/2] lib-sieve: storage: file - Refuse symlinks escaping + personal storage directory + +Pigeonhole's file storage followed any symlink encountered while resolving a +script path, including symlinks in personal (user-writable) storage whose +target lay outside the storage directory. In some non-recommended +configurations a user could exploit this through the include extension: +an "include :personal name;" lookup of ~/sieve/name.sieve transparently +followed a user-placed to e.g. another user's file readable by the mail +process, leaking its contents (or causing it to be parsed as Sieve). +Normally this shouldn't be possible, because sieve processes shouldn't +have any more privileges to read files than the local system user creating +the symlink. + +Open the canonical (realpath'd) personal storage directory at storage init +time and keep an O_DIRECTORY|O_CLOEXEC fd to it. Resolve script content +reads through this fd by routing sieve_file_script_get_stream() via a new +sieve_file_storage_open_safe() wrapper around t_openat_safe(), which: + + - opens each path component with O_NOFOLLOW so symlinks are detected + explicitly rather than transparently followed; + - follows symlinks only when their (recursively resolved) target stays + beneath dir_fd, refusing absolute targets and `..` past the storage + root with ELOOP; + - caps the symlink-hop count to bound resolution time. + +Anchoring at dir_fd makes the lookup TOCTOU-safe even when intermediate +path components are mutated on disk concurrently: resolution stays +relative to the original directory inode and the safe walker still rejects +any target that leaves it. + +Apply the protection only when storage->is_personal is set; admin-managed +global storage is trusted and may legitimately use cross-boundary symlinks. +Single-file storages (is_file=TRUE) keep dir_fd at -1 and fall back to the +existing open path. Also guard the dir_fd open with S_ISDIR() to handle the +autodetect quirk where storage_path can refer to a regular file even when +is_file is FALSE. +--- + .../storage/file/sieve-file-script.c | 39 ++++++++-- + .../storage/file/sieve-file-storage.c | 74 +++++++++++++++++++ + .../storage/file/sieve-file-storage.h | 32 ++++++++ + 3 files changed, 140 insertions(+), 5 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c ++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c +@@ -441,15 +441,44 @@ sieve_file_script_get_stream(struct siev + { + struct sieve_file_script *fscript = + container_of(script, struct sieve_file_script, script); ++ struct sieve_file_storage *fstorage = ++ container_of(script->storage, struct sieve_file_storage, ++ storage); + struct stat st; + struct istream *result; ++ const char *error; + int fd; + +- fd = open(fscript->path, O_RDONLY); +- if (fd < 0) { +- sieve_file_script_handle_error(fscript, "open", fscript->path, +- fscript->script.name); +- return -1; ++ /* For directory-based storage, open the script via the storage ++ directory fd so that path resolution refuses to follow symlinks ++ whose (recursive) target leaves the storage directory. ++ Single-file storages have no dir_fd, so fall back to plain open(). */ ++ if (fstorage->dir_fd >= 0 && fscript->filename != NULL && ++ *fscript->filename != '\0') { ++ if (sieve_file_storage_open_safe(fstorage, fscript->filename, ++ O_RDONLY, &fd, &error) < 0) { ++ if (errno == ELOOP) { ++ sieve_script_set_critical( ++ script, ++ "Failed to open sieve script: %s", ++ error); ++ script->storage->error_code = ++ SIEVE_ERROR_NO_PERMISSION; ++ return -1; ++ } ++ sieve_file_script_handle_error(fscript, "open", ++ fscript->path, ++ fscript->script.name); ++ return -1; ++ } ++ } else { ++ fd = open(fscript->path, O_RDONLY); ++ if (fd < 0) { ++ sieve_file_script_handle_error(fscript, "open", ++ fscript->path, ++ fscript->script.name); ++ return -1; ++ } + } + + if (fstat(fd, &st) != 0) { +Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c ++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c +@@ -20,6 +20,7 @@ + #include + #include + #include ++#include + #include + #include + +@@ -42,6 +43,33 @@ sieve_file_storage_path_extend(struct si + return t_strconcat(path, "/", filename , NULL); + } + ++int sieve_file_storage_open_safe(struct sieve_file_storage *fstorage, ++ const char *path, int flags, int *fd_r, ++ const char **error_r) ++{ ++ const char *walk_error; ++ int fd; ++ ++ *fd_r = -1; ++ ++ if (fstorage->dir_fd < 0) { ++ *error_r = "storage directory fd not available"; ++ errno = ENOTSUP; ++ return -1; ++ } ++ ++ fd = t_openat_safe(fstorage->dir_fd, path, flags, &walk_error); ++ if (fd < 0) { ++ *error_r = t_strdup_printf( ++ "Failed to open '%s/%s': %s", ++ fstorage->path, path, walk_error); ++ return -1; ++ } ++ ++ *fd_r = fd; ++ return 0; ++} ++ + /* + * + */ +@@ -215,10 +243,19 @@ static struct sieve_storage *sieve_file_ + fstorage = p_new(pool, struct sieve_file_storage, 1); + fstorage->storage = sieve_file_storage; + fstorage->storage.pool = pool; ++ fstorage->dir_fd = -1; + + return &fstorage->storage; + } + ++static void sieve_file_storage_destroy(struct sieve_storage *storage) ++{ ++ struct sieve_file_storage *fstorage = ++ container_of(storage, struct sieve_file_storage, storage); ++ ++ i_close_fd(&fstorage->dir_fd); ++} ++ + static int + sieve_file_storage_get_full_path(struct sieve_file_storage *fstorage, + const char **storage_path) +@@ -436,6 +473,42 @@ sieve_file_storage_init_common(struct si + fstorage->link_path = + p_strdup(storage->pool, link_path); + } ++ ++ /* For personal (user-writable) storage, open a fd to the ++ canonical storage directory. This fd anchors TOCTOU-safe ++ path resolution for script lookups: the inode it refers to ++ cannot change underneath us, so even if the user mutates ++ intermediate path components on disk afterwards, we still ++ resolve relative to the original directory. The walker that ++ uses this fd refuses script paths that escape the storage ++ directory through symlinks or `..`, which would otherwise ++ let an unprivileged user redirect e.g. include "name" to a ++ file outside their own sieve storage. ++ ++ Non-personal (e.g. admin-configured global) storage is ++ trusted; dir_fd stays unset there so legitimate ++ admin-managed symlinks crossing the storage boundary keep ++ working. ++ ++ Only open the fd when storage_path actually resolves to a ++ directory: autodetect can leave is_file=FALSE while ++ storage_path still points at a regular file (see the ++ storage_path == active_path fallback in ++ sieve_file_storage_do_autodetect()). In that case there is ++ no directory to anchor to and lookups by name will fail ++ with ENOENT later anyway. */ ++ if (storage->is_personal && ++ S_ISDIR(fstorage->st.st_mode)) { ++ fstorage->dir_fd = open(storage_path, ++ O_RDONLY | O_DIRECTORY | ++ O_CLOEXEC); ++ if (fstorage->dir_fd < 0) { ++ sieve_storage_set_critical(storage, ++ "Failed to open storage directory: " ++ "open(%s) failed: %m", storage_path); ++ return -1; ++ } ++ } + } + + fstorage->path = p_strdup(storage->pool, storage_path); +@@ -904,6 +977,7 @@ const struct sieve_storage sieve_file_st + .v = { + .alloc = sieve_file_storage_alloc, + .init = sieve_file_storage_init, ++ .destroy = sieve_file_storage_destroy, + + .autodetect = sieve_file_storage_autodetect, + +Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.h +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.h ++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.h +@@ -41,6 +41,13 @@ struct sieve_file_storage { + + time_t prev_mtime; + ++ /* fd referencing the canonical (realpath'd) storage directory. Used to ++ anchor TOCTOU-safe path resolution for script files: lookups that ++ escape this directory via symlinks (or `..`) are refused. -1 if the ++ storage is a single-file storage or this protection is unavailable. ++ */ ++ int dir_fd; ++ + bool is_file:1; + }; + +@@ -60,6 +67,31 @@ int sieve_file_storage_init_from_path(st + + int sieve_file_storage_pre_modify(struct sieve_storage *storage); + ++/* Open a script file that lives under the storage directory, refusing any ++ path resolution that escapes that directory through symlinks or `..`. ++ ++ The path is resolved component-by-component using openat() relative to ++ fstorage->dir_fd, with O_NOFOLLOW per component. Symlinks are followed ++ only if their (recursively resolved) target also stays beneath dir_fd; ++ absolute symlink targets and `..` past the storage root are refused. ++ ++ Anchoring the resolution at dir_fd makes the check TOCTOU-safe: even if ++ the user mutates path components on disk between calls, dir_fd still ++ refers to the original directory inode. ++ ++ `flags` is OR-ed into the openat() call for the final component (e.g. ++ O_RDONLY). O_NOFOLLOW and O_CLOEXEC are added automatically. ++ ++ Returns 0 on success and stores the new fd in *fd_r. Returns -1 on ++ failure with errno set; *error_r is set to a descriptive message. ++ errno=ELOOP indicates either too many symlinks or an attempted escape. ++ errno=ENOTSUP indicates fstorage->dir_fd is not available; the caller ++ may fall back to opening fstorage->path directly. ++ */ ++int sieve_file_storage_open_safe(struct sieve_file_storage *fstorage, ++ const char *path, int flags, int *fd_r, ++ const char **error_r); ++ + /* Active script */ + + int sieve_file_storage_active_replace_link(struct sieve_file_storage *fstorage, diff --git a/patches/0001-lib-sieve-util-edit-mail-Fix-writing-to-freed-memory.patch b/patches/0001-lib-sieve-util-edit-mail-Fix-writing-to-freed-memory.patch new file mode 100644 index 0000000..3eeaace --- /dev/null +++ b/patches/0001-lib-sieve-util-edit-mail-Fix-writing-to-freed-memory.patch @@ -0,0 +1,128 @@ +From adcd0f9553c6d07142b97766fa0b53d56112c15c Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 14 Apr 2026 12:58:08 +0200 +Subject: [PATCH 1/3] lib-sieve: util: edit-mail - Fix writing to freed memory + +--- + Makefile.am | 1 + + src/lib-sieve/util/edit-mail.c | 12 ++- + .../editheader/deleteheader-snapshot.svtest | 76 +++++++++++++++++++ + 3 files changed, 88 insertions(+), 1 deletion(-) + create mode 100644 tests/extensions/editheader/deleteheader-snapshot.svtest + +Index: trixie/pigeonhole/Makefile.am +=================================================================== +--- trixie.orig/pigeonhole/Makefile.am ++++ trixie/pigeonhole/Makefile.am +@@ -164,6 +164,7 @@ test_cases = \ + tests/extensions/ihave/restrictions.svtest \ + tests/extensions/editheader/addheader.svtest \ + tests/extensions/editheader/deleteheader.svtest \ ++ tests/extensions/editheader/deleteheader-snapshot.svtest \ + tests/extensions/editheader/alternating.svtest \ + tests/extensions/editheader/utf8.svtest \ + tests/extensions/editheader/protected.svtest \ +Index: trixie/pigeonhole/src/lib-sieve/util/edit-mail.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/util/edit-mail.c ++++ trixie/pigeonhole/src/lib-sieve/util/edit-mail.c +@@ -890,7 +890,17 @@ static int edit_mail_headers_parse(struc + } + } + +- /* Rebuild header index */ ++ /* Rebuild header index. Reset first pointers before rebuilding so that ++ the actual first occurrence in the final list is used. Without this, ++ a snapshot's pre-set first pointer may refer to an appended field ++ that sits later in the list than a parsed field with the same header, ++ causing deleteheader to miss the parsed fields while still freeing ++ the header_index — leaving stale field_idx->header pointers. */ ++ struct _header_index *hidx = edmail->headers_head; ++ while (hidx != NULL) { ++ hidx->first = NULL; ++ hidx = hidx->next; ++ } + current = edmail->header_fields_head; + while (current != NULL) { + if (current->header->first == NULL) +Index: trixie/pigeonhole/tests/extensions/editheader/deleteheader-snapshot.svtest +=================================================================== +--- /dev/null ++++ trixie/pigeonhole/tests/extensions/editheader/deleteheader-snapshot.svtest +@@ -0,0 +1,76 @@ ++require "vnd.dovecot.testsuite"; ++require "editheader"; ++require "fileinto"; ++require "mailbox"; ++ ++/* ++ * Regression test: heap use-after-free in edit_mail_snapshot when ++ * deleteheader is called after a fileinto snapshot on an edit_mail ++ * whose headers have not yet been parsed. ++ */ ++ ++test_set "message" text: ++From: sender@example.com ++To: recipient@example.com ++Cc: original@example.com ++Subject: Test ++ ++Body. ++. ++; ++ ++test "deleteheader all: snapshot stale first pointer after parse" { ++ /* Add a Cc header while headers are not yet parsed. The snapshot ++ * created by the next fileinto has H_clone_cc->first pointing at ++ * this appended field. */ ++ addheader :last "Cc" "appended@example.com"; ++ ++ /* fileinto sets edit_snapshot=TRUE. The next call to ++ * sieve_message_edit creates a snapshot (edmail_snap) and the ++ * fileinto action retains the pre-snapshot version for delivery. */ ++ fileinto :create "folder1"; ++ ++ /* sieve_message_edit creates edmail_snap here. edit_mail_headers_parse ++ * inserts the parsed "Cc: original@example.com" before the appended ++ * field in the list. Without the fix the rebuild skips resetting ++ * ->first; the delete loop then only removes the appended field, ++ * frees H_clone_cc (index==0), and leaves the parsed field_idx with ++ * a dangling ->header pointer. */ ++ deleteheader "Cc"; ++ ++ /* A second fileinto + edit operation takes another snapshot of the ++ * same edmail_snap. edit_mail_snapshot walks header_fields_head and ++ * reads field_idx->header->header on the orphaned entry. ++ * Without the fix: use-after-free crash. ++ * With the fix: clean run, both Cc occurrences are correctly gone. */ ++ fileinto :create "folder2"; ++ deleteheader "Subject"; ++ ++ if not test_result_execute { ++ test_fail "failed to execute result"; ++ } ++ ++ /* folder1 was snapshotted before deleteheader "Cc": both Cc headers ++ * must be present. */ ++ if not test_message :folder "folder1" 0 { ++ test_fail "message not stored in folder1"; ++ } ++ ++ if not header :is "Cc" "original@example.com" { ++ test_fail "original Cc missing in folder1 snapshot"; ++ } ++ ++ if not header :is "Cc" "appended@example.com" { ++ test_fail "appended Cc missing in folder1 snapshot"; ++ } ++ ++ /* folder2 was snapshotted after deleteheader "Cc": both Cc headers ++ * must be absent. */ ++ if not test_message :folder "folder2" 0 { ++ test_fail "message not stored in folder2"; ++ } ++ ++ if exists "Cc" { ++ test_fail "Cc header not fully deleted in folder2 snapshot"; ++ } ++} diff --git a/patches/0001-lib-sql-Add-sql_result_new_error-helper.patch b/patches/0001-lib-sql-Add-sql_result_new_error-helper.patch new file mode 100644 index 0000000..eeead61 --- /dev/null +++ b/patches/0001-lib-sql-Add-sql_result_new_error-helper.patch @@ -0,0 +1,66 @@ +From 7aa8c147eb15009b56c85a00c33c2c0b972c1398 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Fri, 29 May 2026 11:21:00 +0000 +Subject: [PATCH] lib-sql: Add sql_result_new_error() helper + +--- + src/lib-sql/sql-api.c | 43 +++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 43 insertions(+) + +diff --git a/src/lib-sql/sql-api.c b/src/lib-sql/sql-api.c +index 3d4518c3cc..4db6913229 100644 +--- a/src/lib-sql/sql-api.c ++++ b/src/lib-sql/sql-api.c +@@ -66,6 +66,49 @@ ARRAY_TYPE(sql_drivers) sql_drivers; + static void sql_query_delayed_callback(struct sql_query_result_delayed *cb); + static void sql_commit_delayed_callback(struct sql_commit_result_delayed *cb); + ++struct sql_result_error { ++ struct sql_result result; ++ char *error; ++}; ++ ++static void sql_result_error_free(struct sql_result *_result) ++{ ++ struct sql_result_error *result = ++ container_of(_result, struct sql_result_error, result); ++ i_free(result->error); ++ i_free(result); ++} ++ ++static int sql_result_error_next_row(struct sql_result *result ATTR_UNUSED) ++{ ++ return -1; ++} ++ ++static const char * ++sql_result_error_get_error(struct sql_result *_result) ++{ ++ struct sql_result_error *result = ++ container_of(_result, struct sql_result_error, result); ++ return result->error; ++} ++ ++static const struct sql_result_vfuncs sql_result_error_vfuncs = { ++ .free = sql_result_error_free, ++ .next_row = sql_result_error_next_row, ++ .get_error = sql_result_error_get_error, ++}; ++ ++static struct sql_result *sql_result_new_error(const char *error) ATTR_UNUSED; ++static struct sql_result *sql_result_new_error(const char *error) ++{ ++ struct sql_result_error *result = i_new(struct sql_result_error, 1); ++ result->result.v = sql_result_error_vfuncs; ++ result->result.failed = TRUE; ++ result->result.refcount = 1; ++ result->error = i_strdup(error); ++ return &result->result; ++} ++ + void sql_drivers_init_without_drivers(void) + { + i_array_init(&sql_drivers, 8); +-- +2.47.3 + diff --git a/patches/0001-lib-sql-Use-strchr-based-while-loop-to-fill-template.patch b/patches/0001-lib-sql-Use-strchr-based-while-loop-to-fill-template.patch new file mode 100644 index 0000000..a953082 --- /dev/null +++ b/patches/0001-lib-sql-Use-strchr-based-while-loop-to-fill-template.patch @@ -0,0 +1,100 @@ +From 98282f1a02d29d6fdd0b3db63ae398d0162fe7bd Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 18 Aug 2025 08:30:42 +0300 +Subject: [PATCH] lib-sql: Use strchr() based while loop to fill template + +This is much faster than going one byte at a time. +--- + src/lib-sql/sql-api.c | 26 ++++++++++++++------------ + src/lib-sql/test-sql.c | 30 ++++++++++++++++++++++++++++++ + 2 files changed, 44 insertions(+), 12 deletions(-) + +Index: trixie/src/lib-sql/sql-api.c +=================================================================== +--- trixie.orig/src/lib-sql/sql-api.c ++++ trixie/src/lib-sql/sql-api.c +@@ -400,22 +400,24 @@ const char *sql_statement_get_query(stru + { + string_t *query = t_str_new(128); + const char *const *args; +- unsigned int i, args_count, arg_pos = 0; ++ unsigned int args_count, arg_pos = 0; ++ const char *p0, *p1; + + args = array_get(&stmt->args, &args_count); +- +- for (i = 0; stmt->query_template[i] != '\0'; i++) { +- if (stmt->query_template[i] == '?') { +- if (arg_pos >= args_count || +- args[arg_pos] == NULL) { +- i_panic("lib-sql: Missing bind for arg #%u in statement: %s", +- arg_pos, stmt->query_template); +- } +- str_append(query, args[arg_pos++]); +- } else { +- str_append_c(query, stmt->query_template[i]); ++ p0 = stmt->query_template; ++ while ((p1 = strchr(p0, '?')) != NULL) { ++ /* append until ? */ ++ str_append_max(query, p0, (p1 - p0)); ++ if (arg_pos >= args_count || ++ args[arg_pos] == NULL) { ++ i_panic("lib-sql: Missing bind for arg #%u in statement: %s", ++ arg_pos, stmt->query_template); + } ++ str_append(query, args[arg_pos++]); ++ p0 = p1 + 1; + } ++ str_append(query, p0); ++ + if (arg_pos != args_count) { + i_panic("lib-sql: Too many bind args (%u) for statement: %s", + args_count, stmt->query_template); +Index: trixie/src/lib-sql/test-sql.c +=================================================================== +--- trixie.orig/src/lib-sql/test-sql.c ++++ trixie/src/lib-sql/test-sql.c +@@ -60,6 +60,24 @@ static void deinit_sql(struct sql_db **_ + }; \ + sql_driver_test_add_expected_result(sql, &result_1); + ++#define setup_result_2(sql) \ ++ struct test_driver_result_set rset_2 = { \ ++ .rows = 2, \ ++ .cols = 1, \ ++ .col_names = (const char *[]){"foo", NULL}, \ ++ .row_data = (const char **[]){ \ ++ (const char*[]){"value1", NULL}, \ ++ (const char*[]){"value2", NULL}, \ ++ }, \ ++ }; \ ++ struct test_driver_result result_2 = { \ ++ .nqueries = 1, \ ++ .queries = (const char *[]){"SELECT foo FROM bar WHERE baz = 'foz'"}, \ ++ .result = &rset_2 \ ++ }; \ ++ sql_driver_test_add_expected_result(sql, &result_2); ++ ++ + static void test_result_1(struct sql_result *cursor) + { + test_assert(sql_result_next_row(cursor) == SQL_RESULT_NEXT_OK); +@@ -125,6 +143,18 @@ static void test_sql_stmt_prepared_api(v + + test_result_1(cursor); + ++ sql_result_unref(cursor); ++ ++ setup_result_2(sql); ++ ++ prep_stmt = sql_prepared_statement_init(sql, "SELECT foo FROM bar WHERE baz = ?"); ++ stmt = sql_statement_init_prepared(prep_stmt); ++ sql_statement_bind_str(stmt, 0, "foz"); ++ sql_prepared_statement_unref(&prep_stmt); ++ cursor = sql_statement_query_s(&stmt); ++ ++ test_result_1(cursor); ++ + sql_result_unref(cursor); + + deinit_sql(&sql); diff --git a/patches/0001-lib-sql-driver-sqlite-Use-sqlite3_snprintf-to-quote-.patch b/patches/0001-lib-sql-driver-sqlite-Use-sqlite3_snprintf-to-quote-.patch new file mode 100644 index 0000000..68c0782 --- /dev/null +++ b/patches/0001-lib-sql-driver-sqlite-Use-sqlite3_snprintf-to-quote-.patch @@ -0,0 +1,51 @@ +From ff926d0c9288f343d18a3c9ec20cf1e252d52286 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Fri, 7 Nov 2025 09:21:01 +0200 +Subject: [PATCH] lib-sql: driver-sqlite - Use sqlite3_snprintf() to quote + values + +This does it the sqlite3 way. +--- + src/lib-sql/driver-sqlite.c | 29 +++++------------------------ + 1 file changed, 5 insertions(+), 24 deletions(-) + +Index: trixie/src/lib-sql/driver-sqlite.c +=================================================================== +--- trixie.orig/src/lib-sql/driver-sqlite.c ++++ trixie/src/lib-sql/driver-sqlite.c +@@ -225,30 +225,11 @@ static const char * + driver_sqlite_escape_string(struct sql_db *_db ATTR_UNUSED, + const char *string) + { +- const char *p; +- char *dest, *destbegin; +- +- /* find the first ' */ +- for (p = string; *p != '\''; p++) { +- if (*p == '\0') +- return t_strdup_noconst(string); +- } +- +- /* @UNSAFE: escape ' with '' */ +- dest = destbegin = t_buffer_get((p - string) + strlen(string) * 2 + 1); +- +- memcpy(dest, string, p - string); +- dest += p - string; +- +- for (; *p != '\0'; p++) { +- *dest++ = *p; +- if (*p == '\'') +- *dest++ = *p; +- } +- *dest++ = '\0'; +- t_buffer_alloc(dest - destbegin); +- +- return destbegin; ++ const size_t len = strlen(string) * 2 + 1; ++ char *escaped = t_malloc_no0(len); ++ if (sqlite3_snprintf(len, escaped, "%q", string) == NULL) ++ i_unreached(); ++ return escaped; + } + + static const char * diff --git a/patches/0001-lib-sql-sql-api-Implement-async-calls-for-drivers-th.patch b/patches/0001-lib-sql-sql-api-Implement-async-calls-for-drivers-th.patch new file mode 100644 index 0000000..70a2714 --- /dev/null +++ b/patches/0001-lib-sql-sql-api-Implement-async-calls-for-drivers-th.patch @@ -0,0 +1,247 @@ +From a78c015eae67a891a0c14b5434dd1f99838ae4cc Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 6 Oct 2025 12:39:50 +0300 +Subject: [PATCH] lib-sql: sql-api - Implement async calls for drivers that + can't + +Changes the behaviour of asynchronous functions to be truly asynchronous +even if the underlying driver isn't capable of doing this. Implemented +by adding immediate timeouts to call the callbacks after returning from the +synchronous function and ending up back to ioloop. +--- + src/lib-sql/sql-api-private.h | 7 +++ + src/lib-sql/sql-api.c | 103 ++++++++++++++++++++++++++++++++-- + src/lib-sql/sql-api.h | 4 +- + 3 files changed, 107 insertions(+), 7 deletions(-) + +Index: trixie/src/lib-sql/sql-api-private.h +=================================================================== +--- trixie.orig/src/lib-sql/sql-api-private.h ++++ trixie/src/lib-sql/sql-api-private.h +@@ -84,11 +84,15 @@ struct sql_db_vfuncs { + const char *(*escape_string)(struct sql_db *db, const char *string); + + void (*exec)(struct sql_db *db, const char *query); ++ /* Only implement this if the driver can really do asynchronous callbacks, ++ otherwise let sql API handle it. */ + void (*query)(struct sql_db *db, const char *query, + sql_query_callback_t *callback, void *context); + struct sql_result *(*query_s)(struct sql_db *db, const char *query); + + struct sql_transaction_context *(*transaction_begin)(struct sql_db *db); ++ /* Only implement this if the driver can really do asynchronous callbacks, ++ otherwise let sql API handle it. */ + void (*transaction_commit)(struct sql_transaction_context *ctx, + sql_commit_callback_t *callback, + void *context); +@@ -154,6 +158,9 @@ struct sql_db { + struct event *event; + HASH_TABLE(char *, struct sql_prepared_statement *) prepared_stmt_hash; + ++ struct sql_query_result_delayed *query_delayed_list; ++ struct sql_commit_result_delayed *commit_delayed_list; ++ + enum sql_db_state state; + /* last time we started connecting to this server + (which may or may not have succeeded) */ +Index: trixie/src/lib-sql/sql-api.c +=================================================================== +--- trixie.orig/src/lib-sql/sql-api.c ++++ trixie/src/lib-sql/sql-api.c +@@ -4,6 +4,7 @@ + #include "array.h" + #include "ioloop.h" + #include "hash.h" ++#include "llist.h" + #include "str.h" + #include "time-util.h" + #include "settings.h" +@@ -12,6 +13,28 @@ + + #include + ++struct sql_query_result_delayed { ++ struct sql_query_result_delayed *prev, *next; ++ ++ struct sql_db *db; ++ struct timeout *to; ++ ++ struct sql_result *result; ++ sql_query_callback_t *callback; ++ void *context; ++}; ++ ++struct sql_commit_result_delayed { ++ struct sql_commit_result_delayed *prev, *next; ++ ++ struct sql_db *db; ++ struct timeout *to; ++ ++ char *error; ++ sql_commit_callback_t *callback; ++ void *context; ++}; ++ + struct event_category event_category_sql = { + .name = "sql", + }; +@@ -40,6 +63,9 @@ const struct setting_parser_info sql_set + struct sql_db_module_register sql_db_module_register = { 0 }; + ARRAY_TYPE(sql_drivers) sql_drivers; + ++static void sql_query_delayed_callback(struct sql_query_result_delayed *cb); ++static void sql_commit_delayed_callback(struct sql_commit_result_delayed *cb); ++ + void sql_drivers_init_without_drivers(void) + { + i_array_init(&sql_drivers, 8); +@@ -228,9 +254,19 @@ int sql_connect(struct sql_db *db) + return db->v.connect(db); + } + ++static void sql_call_delayed_callbacks(struct sql_db *db) ++{ ++ /* flush any pending results */ ++ while (db->query_delayed_list != NULL) ++ sql_query_delayed_callback(db->query_delayed_list); ++ while (db->commit_delayed_list != NULL) ++ sql_commit_delayed_callback(db->commit_delayed_list); ++} ++ + void sql_disconnect(struct sql_db *db) + { + timeout_remove(&db->to_reconnect); ++ sql_call_delayed_callbacks(db); + db->v.disconnect(db); + } + +@@ -250,11 +286,31 @@ void sql_exec(struct sql_db *db, const c + db->v.exec(db, query); + } + ++static void sql_query_delayed_callback(struct sql_query_result_delayed *cb) ++{ ++ timeout_remove(&cb->to); ++ DLLIST_REMOVE(&cb->db->query_delayed_list, cb); ++ cb->callback(cb->result, cb->context); ++ sql_result_unref(cb->result); ++ i_free(cb); ++} ++ + #undef sql_query + void sql_query(struct sql_db *db, const char *query, + sql_query_callback_t *callback, void *context) + { +- db->v.query(db, query, callback, context); ++ if (db->v.query != NULL) { ++ db->v.query(db, query, callback, context); ++ return; ++ } ++ ++ struct sql_query_result_delayed *cb = i_new(struct sql_query_result_delayed, 1); ++ cb->db = db; ++ cb->result = sql_query_s(db, query); ++ cb->callback = callback; ++ cb->context = context; ++ cb->to = timeout_add_short(0, sql_query_delayed_callback, cb); ++ DLLIST_PREPEND(&db->query_delayed_list, cb); + } + + struct sql_result *sql_query_s(struct sql_db *db, const char *query) +@@ -494,11 +550,21 @@ void sql_statement_query(struct sql_stat + sql_query_callback_t *callback, void *context) + { + struct sql_statement *stmt = *_stmt; +- + *_stmt = NULL; ++ + if (stmt->db->v.statement_query != NULL) + stmt->db->v.statement_query(stmt, callback, context); +- else ++ else if (stmt->db->v.statement_query_s != NULL) { ++ struct sql_db *db = stmt->db; ++ struct sql_query_result_delayed *cb = ++ i_new(struct sql_query_result_delayed, 1); ++ cb->db = db; ++ cb->callback = callback; ++ cb->context = context; ++ cb->result = sql_statement_query_s(&stmt); ++ cb->to = timeout_add_short(0, sql_query_delayed_callback, cb); ++ DLLIST_PREPEND(&db->query_delayed_list, cb); ++ } else + default_sql_statement_query(stmt, callback, context); + } + +@@ -755,14 +821,40 @@ void sql_transaction_set_non_atomic(stru + ctx->non_atomic = TRUE; + } + ++static void sql_commit_delayed_callback(struct sql_commit_result_delayed *cb) ++{ ++ struct sql_commit_result result = { ++ .error = cb->error, ++ }; ++ timeout_remove(&cb->to); ++ DLLIST_REMOVE(&cb->db->commit_delayed_list, cb); ++ cb->callback(&result, cb->context); ++ i_free(cb->error); ++ i_free(cb); ++} ++ + #undef sql_transaction_commit + void sql_transaction_commit(struct sql_transaction_context **_ctx, + sql_commit_callback_t *callback, void *context) + { + struct sql_transaction_context *ctx = *_ctx; +- ++ struct sql_db *db = ctx->db; + *_ctx = NULL; +- ctx->db->v.transaction_commit(ctx, callback, context); ++ ++ if (ctx->db->v.transaction_commit != NULL) { ++ ctx->db->v.transaction_commit(ctx, callback, context); ++ return; ++ } ++ ++ struct sql_commit_result_delayed *cb = i_new(struct sql_commit_result_delayed, 1); ++ const char *error = NULL; ++ ctx->db->v.transaction_commit_s(ctx, &error); ++ cb->db = db; ++ cb->error = i_strdup(error); ++ cb->callback = callback; ++ cb->context = context; ++ cb->to = timeout_add_short(0, sql_commit_delayed_callback, cb); ++ DLLIST_PREPEND(&db->commit_delayed_list, cb); + } + + int sql_transaction_commit_s(struct sql_transaction_context **_ctx, +@@ -900,6 +992,7 @@ void sql_wait(struct sql_db *db) + { + if (db->v.wait != NULL) + db->v.wait(db); ++ sql_call_delayed_callbacks(db); + } + + +Index: trixie/src/lib-sql/sql-api.h +=================================================================== +--- trixie.orig/src/lib-sql/sql-api.h ++++ trixie/src/lib-sql/sql-api.h +@@ -123,7 +123,7 @@ const char *sql_escape_blob(struct sql_d + void sql_exec(struct sql_db *db, const char *query); + /* Execute SQL query and return result in callback. If fields list is given, + the returned fields are validated to be of correct type, and you can use +- sql_result_next_row_get() */ ++ sql_result_next_row_get(). The callback is never called immediately. */ + void sql_query(struct sql_db *db, const char *query, + sql_query_callback_t *callback, void *context); + #define sql_query(db, query, callback, context) \ +@@ -229,7 +229,7 @@ struct sql_transaction_context *sql_tran + /* Don't require transaction to be atomic. Currently this is implemented only + with Cassandra to use UNLOGGED BATCH operations. */ + void sql_transaction_set_non_atomic(struct sql_transaction_context *ctx); +-/* Commit transaction. */ ++/* Commit transaction. The callback is never called immediately. */ + void sql_transaction_commit(struct sql_transaction_context **ctx, + sql_commit_callback_t *callback, void *context); + #define sql_transaction_commit(ctx, callback, context) \ diff --git a/patches/0001-lib-storage-Allow-NULL-path-in-mailbox_list_mkdir_ro.patch b/patches/0001-lib-storage-Allow-NULL-path-in-mailbox_list_mkdir_ro.patch new file mode 100644 index 0000000..8963bd8 --- /dev/null +++ b/patches/0001-lib-storage-Allow-NULL-path-in-mailbox_list_mkdir_ro.patch @@ -0,0 +1,44 @@ +From ca82388f330be27e015ad4b3c08b0156307cf369 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 23 Jun 2026 14:59:45 +0000 +Subject: [PATCH 1/2] lib-storage: Allow NULL path in mailbox_list_mkdir_root() + +When path is NULL, look up the root directory path of the given type via +mailbox_list_get_root_path() and create that. If there is no path of the +requested type, nothing is done. This avoids open-coding the common +get_root_path() + mkdir_root() pair at call sites. +--- + src/lib-storage/mailbox-list.c | 4 ++++ + src/lib-storage/mailbox-list.h | 4 +++- + 2 files changed, 7 insertions(+), 1 deletion(-) + +Index: trixie/src/lib-storage/mailbox-list.c +=================================================================== +--- trixie.orig/src/lib-storage/mailbox-list.c ++++ trixie/src/lib-storage/mailbox-list.c +@@ -1019,6 +1019,10 @@ int mailbox_list_mkdir_root(struct mailb + { + const char *error; + ++ if (path == NULL && ++ !mailbox_list_get_root_path(list, type, &path)) ++ return 0; ++ + if (mailbox_list_try_mkdir_root(list, path, type, &error) < 0) { + mailbox_list_set_critical(list, "%s", error); + return -1; +Index: trixie/src/lib-storage/mailbox-list.h +=================================================================== +--- trixie.orig/src/lib-storage/mailbox-list.h ++++ trixie/src/lib-storage/mailbox-list.h +@@ -177,7 +177,9 @@ void mailbox_list_get_permissions(struct + void mailbox_list_get_root_permissions(struct mailbox_list *list, + struct mailbox_permissions *permissions_r); + /* mkdir() a root directory of given type with proper permissions. The path can +- be either the root itself or point to a directory under the root. */ ++ be either the root itself or point to a directory under the root. If path is ++ NULL, the root path of the given type is looked up automatically; if there is ++ no such path, nothing is done. */ + int mailbox_list_mkdir_root(struct mailbox_list *list, const char *path, + enum mailbox_list_path_type type); + /* Like mailbox_list_mkdir_root(), but don't log an error if it fails. */ diff --git a/patches/0001-lib-storage-thread-Avoid-excessive-data-stack-growth.patch b/patches/0001-lib-storage-thread-Avoid-excessive-data-stack-growth.patch new file mode 100644 index 0000000..5e0fb99 --- /dev/null +++ b/patches/0001-lib-storage-thread-Avoid-excessive-data-stack-growth.patch @@ -0,0 +1,55 @@ +From 7774d50c731fa97e445fd9f4cb9e4f08e4415f8b Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 13 Apr 2026 17:28:43 +0200 +Subject: [PATCH 1/3] lib-storage: thread - Avoid excessive data stack growth + with many References msgids + +--- + src/lib-storage/index/index-thread.c | 22 +++++++++++++++------- + 1 file changed, 15 insertions(+), 7 deletions(-) + +diff --git a/src/lib-storage/index/index-thread.c b/src/lib-storage/index/index-thread.c +index 2b1ba045ac..e20a97072c 100644 +--- a/src/lib-storage/index/index-thread.c ++++ b/src/lib-storage/index/index-thread.c +@@ -93,9 +93,11 @@ mail_strmap_rec_get_msgid(struct mail_thread_context *ctx, + + /* get the nth message-id */ + msgid = message_id_get_next(&msgids); +- if (msgid != NULL) { +- for (; n > 0; n--) ++ if (msgid != NULL && n > 0) { ++ for (; n > 1; n--) T_BEGIN { + msgid = message_id_get_next(&msgids); ++ } T_END; ++ msgid = message_id_get_next(&msgids); + } + + if (msgid == NULL) { +@@ -266,12 +268,18 @@ mail_thread_map_add_mail(struct mail_thread_context *ctx, struct mail *mail) + msgid = message_id_get_next(&references); + if (msgid != NULL) { + ref_index = MAIL_THREAD_NODE_REF_REFERENCES1; ++ mail_index_strmap_view_sync_add(ctx->strmap_sync, ++ mail->uid, ref_index, msgid); + do { +- mail_index_strmap_view_sync_add(ctx->strmap_sync, +- mail->uid, +- ref_index, msgid); +- ref_index++; +- msgid = message_id_get_next(&references); ++ T_BEGIN { ++ ref_index++; ++ msgid = message_id_get_next(&references); ++ if (msgid != NULL) { ++ mail_index_strmap_view_sync_add( ++ ctx->strmap_sync, mail->uid, ++ ref_index, msgid); ++ } ++ } T_END; + } while (msgid != NULL); + } else { + /* no References:, use In-Reply-To: */ +-- +2.47.3 + diff --git a/patches/0001-lib-test-Add-test_assert_memcmp.patch b/patches/0001-lib-test-Add-test_assert_memcmp.patch new file mode 100644 index 0000000..75a3029 --- /dev/null +++ b/patches/0001-lib-test-Add-test_assert_memcmp.patch @@ -0,0 +1,104 @@ +From 4677492d038f5187363790b5a93bd5fccc989a98 Mon Sep 17 00:00:00 2001 +From: Stephan Bosch +Date: Fri, 25 Jul 2025 03:31:44 +0200 +Subject: [PATCH] lib-test: Add test_assert_memcmp*() + +--- + src/lib-test/test-common.c | 33 +++++++++++++++++++++++++++++++++ + src/lib-test/test-common.h | 25 ++++++++++++++++++++++++- + 2 files changed, 57 insertions(+), 1 deletion(-) + +Index: trixie/src/lib-test/test-common.c +=================================================================== +--- trixie.orig/src/lib-test/test-common.c ++++ trixie/src/lib-test/test-common.c +@@ -84,6 +84,39 @@ void test_assert_failed_strcmp_idx(const + #endif + } + ++static void print_memcmp_data(const void *_data, size_t len) ++{ ++ const unsigned char *data = _data; ++ size_t i; ++ ++ for (i = 0; i < len; i++) { ++ if (data[i] >= 0x20 && data[i] < 0x7f) ++ printf("%c", data[i]); ++ else ++ printf("\\x%02X", data[i]); ++ } ++} ++ ++void test_assert_failed_memcmp_idx(const char *code, const char *file, unsigned int line, ++ const void *src, const void *dst, size_t len, long long i) ++{ ++ printf("%s:%u: Assert", file, line); ++ if (i == LLONG_MIN) ++ printf(" failed: %s\n", code); ++ else ++ printf("(#%lld) failed: %s\n", i, code); ++ printf(" \""); ++ print_memcmp_data(src, len); ++ printf("\" != \""); ++ print_memcmp_data(dst, len); ++ printf("\" (len == %zu)", len); ++ fflush(stdout); ++ test_success = FALSE; ++#ifdef STATIC_CHECKER ++ i_unreached(); ++#endif ++} ++ + void test_assert_failed_cmp_intmax_idx(const char *code, const char *file, + unsigned int line, + intmax_t src, intmax_t dst, +Index: trixie/src/lib-test/test-common.h +=================================================================== +--- trixie.orig/src/lib-test/test-common.h ++++ trixie/src/lib-test/test-common.h +@@ -40,7 +40,14 @@ void test_begin(const char *name); + test_assert_strcmp_idx(s1, s2, LLONG_MIN); \ + } STMT_END + +-/* Same as test_assert_strcmp expect that it takes an additional i as input. ++/* Additional parameters are m1 (source) and m2 (destination) memory and len ++ * in memcmp(). ++ */ ++#define test_assert_memcmp(m1, m2, len) STMT_START { \ ++ test_assert_memcmp_idx(m1, m2, len, LLONG_MIN); \ ++ } STMT_END ++ ++/* Same as test_assert_strcmp except that it takes an additional i as input. + * When i is greater than or equals 0 it is used to identify the barrage of + * tests failed like in test_assert_idx. + */ +@@ -52,6 +59,19 @@ void test_begin(const char *name); + __FILE__, __LINE__, _temp_s1, _temp_s2, i); \ + } STMT_END + ++/* Same as test_assert_memcmp except that it takes an additional i as input. ++ * When i is greater than or equals 0 it is used to identify the barrage of ++ * tests failed like in test_assert_idx. ++*/ ++#define test_assert_memcmp_idx(_m1, _m2, _len, i) STMT_START { \ ++ const void *_temp_m1 = (_m1); \ ++ const void *_temp_m2 = (_m2); \ ++ const size_t _temp_len = (_len); \ ++ if ((memcmp(_temp_m1,_temp_m2, _temp_len) != 0)) \ ++ test_assert_failed_memcmp_idx("memcmp(" #_m1 "," #_m2 "," #_len ")", \ ++ __FILE__, __LINE__, _temp_m1, _temp_m2, _temp_len, i); \ ++ } STMT_END ++ + #define test_assert_cmp_bool(_bool_value1, _op, _value2) \ + test_assert_cmp((unsigned int) _bool_value1, _op, (unsigned int _bool_value2)) + +@@ -92,6 +112,9 @@ void test_assert_failed_idx(const char * + void test_assert_failed_strcmp_idx(const char *code, const char *file, unsigned int line, + const char * src, const char * dst, long long i) + ATTR_STATIC_CHECKER_NORETURN; ++void test_assert_failed_memcmp_idx(const char *code, const char *file, unsigned int line, ++ const void *src, const void *dst, size_t len, long long i) ++ ATTR_STATIC_CHECKER_NORETURN; + void test_assert_failed_cmp_intmax_idx(const char *code, const char *file, + unsigned int line, + intmax_t src, intmax_t dst, diff --git a/patches/0001-lib-test-Require-both-lengths-in-test_assert_memcmp.patch b/patches/0001-lib-test-Require-both-lengths-in-test_assert_memcmp.patch new file mode 100644 index 0000000..8dbd82c --- /dev/null +++ b/patches/0001-lib-test-Require-both-lengths-in-test_assert_memcmp.patch @@ -0,0 +1,45 @@ +From 8f04979a7e72d3b3e04f3a93bd8088f5e2332dd9 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Tue, 27 Jan 2026 19:02:30 +0200 +Subject: [PATCH] lib-test: Require both lengths in test_assert_memcmp() + +--- + src/lib-sql/test-sql-sqlite.c | 2 +- + src/lib-test/test-common.h | 14 ++++++++------ + src/lib/test-unicode-nf.c | 16 ++++++++-------- + 3 files changed, 17 insertions(+), 15 deletions(-) + +Index: trixie/src/lib-test/test-common.h +=================================================================== +--- trixie.orig/src/lib-test/test-common.h ++++ trixie/src/lib-test/test-common.h +@@ -43,8 +43,8 @@ void test_begin(const char *name); + /* Additional parameters are m1 (source) and m2 (destination) memory and len + * in memcmp(). + */ +-#define test_assert_memcmp(m1, m2, len) STMT_START { \ +- test_assert_memcmp_idx(m1, m2, len, LLONG_MIN); \ ++#define test_assert_memcmp(m1, len1, m2, len2) STMT_START { \ ++ test_assert_memcmp_idx(m1, len1, m2, len2, LLONG_MIN); \ + } STMT_END + + /* Same as test_assert_strcmp except that it takes an additional i as input. +@@ -63,12 +63,14 @@ void test_begin(const char *name); + * When i is greater than or equals 0 it is used to identify the barrage of + * tests failed like in test_assert_idx. + */ +-#define test_assert_memcmp_idx(_m1, _m2, _len, i) STMT_START { \ ++#define test_assert_memcmp_idx(_m1, _len1, _m2, _len2, i) STMT_START { \ + const void *_temp_m1 = (_m1); \ + const void *_temp_m2 = (_m2); \ +- const size_t _temp_len = (_len); \ +- if ((memcmp(_temp_m1,_temp_m2, _temp_len) != 0)) \ +- test_assert_failed_memcmp_idx("memcmp(" #_m1 "," #_m2 "," #_len ")", \ ++ const size_t _temp_len = I_MIN((_len1), (_len2)); \ ++ if ((_len1) != (_len2)) \ ++ test_assert_failed_ucmp_intmax_idx(#_len1 " == " #_len2, __FILE__, __LINE__, _len1, _len2, "=", i); \ ++ if ((memcmp(_temp_m1, _temp_m2, _temp_len) != 0)) \ ++ test_assert_failed_memcmp_idx("memcmp(" #_m1 "," #_m2 "," #_len2 ")", \ + __FILE__, __LINE__, _temp_m1, _temp_m2, _temp_len, i); \ + } STMT_END + diff --git a/patches/0001-lib-xxh64-fix-byte-ordering-bug-on-big-endian-system.patch b/patches/0001-lib-xxh64-fix-byte-ordering-bug-on-big-endian-system.patch new file mode 100644 index 0000000..0c31dbc --- /dev/null +++ b/patches/0001-lib-xxh64-fix-byte-ordering-bug-on-big-endian-system.patch @@ -0,0 +1,45 @@ +From 45406e13f1faddf341922ff73962820bc0a99015 Mon Sep 17 00:00:00 2001 +From: Noah Meyerhans +Date: Tue, 1 Sep 2026 21:12:51 -0400 +Subject: [PATCH] lib: xxh64: fix byte ordering bug on big-endian systems + +Convert from little-endian to host byte order where necessary. + +Background at https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1146449 +--- + src/lib/xxh64.c | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/src/lib/xxh64.c b/src/lib/xxh64.c +index 1c17833afd..4a151bd0c2 100644 +--- a/src/lib/xxh64.c ++++ b/src/lib/xxh64.c +@@ -8,6 +8,8 @@ + #include "lib.h" + #include "xxh64.h" + ++#include ++ + #define XXH64_PRIME1 UINT64_C(0x9E3779B185EBCA87) + #define XXH64_PRIME2 UINT64_C(0xC2B2AE3D27D4EB4F) + #define XXH64_PRIME3 UINT64_C(0x165667B19E3779F9) +@@ -20,14 +22,14 @@ static inline uint64_t xxh64_read64(const void *p) + { + uint64_t v; + memcpy(&v, p, sizeof(v)); +- return v; ++ return le64toh(v); /* Convert from little-endian to host byte order */ + } + + static inline uint32_t xxh64_read32(const void *p) + { + uint32_t v; + memcpy(&v, p, sizeof(v)); +- return v; ++ return le32toh(v); /* Convert from little-endian to host byte order */ + } + + static uint64_t ATTR_UNSIGNED_WRAPS xxh64_round(uint64_t acc, uint64_t input) +-- +2.47.3 + diff --git a/patches/0001-login-Add-LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED.patch b/patches/0001-login-Add-LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED.patch new file mode 100644 index 0000000..c617355 --- /dev/null +++ b/patches/0001-login-Add-LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED.patch @@ -0,0 +1,99 @@ +From 3eed5e02c49c2dc1522c038869f723e7c6d93ffc Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 18 Jul 2025 14:28:47 +0300 +Subject: [PATCH] *-login: Add LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED + +--- + src/imap-login/imap-proxy.c | 1 + + src/login-common/client-common.c | 1 + + src/login-common/login-proxy.c | 2 ++ + src/login-common/login-proxy.h | 3 +++ + src/pop3-login/pop3-proxy.c | 1 + + src/submission-login/submission-proxy.c | 1 + + 6 files changed, 9 insertions(+) + +diff --git a/src/imap-login/imap-proxy.c b/src/imap-login/imap-proxy.c +index 69680ea752..03dad690e1 100644 +--- a/src/imap-login/imap-proxy.c ++++ b/src/imap-login/imap-proxy.c +@@ -586,6 +586,7 @@ imap_proxy_send_failure_reply(struct imap_client *imap_client, + break; + case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG: + case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED: + client_send_reply_code(&imap_client->common, IMAP_CMD_REPLY_NO, + IMAP_RESP_CODE_SERVERBUG, + LOGIN_PROXY_FAILURE_MSG); +diff --git a/src/login-common/client-common.c b/src/login-common/client-common.c +index 0c11a925f1..eeb9d6da05 100644 +--- a/src/login-common/client-common.c ++++ b/src/login-common/client-common.c +@@ -1386,6 +1386,7 @@ bool client_get_extra_disconnect_reason(struct client *client, + last_reason = "protocol failure"; + break; + case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED: + event_reason = "auth_failed"; + last_reason = "authentication failure"; + break; +diff --git a/src/login-common/login-proxy.c b/src/login-common/login-proxy.c +index 0e3e8c9be2..10c07b711d 100644 +--- a/src/login-common/login-proxy.c ++++ b/src/login-common/login-proxy.c +@@ -830,6 +830,7 @@ bool login_proxy_failed(struct login_proxy *proxy, struct event *event, + log_prefix = "Remote server sent invalid input: "; + break; + case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED: + case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED: + log_prefix = ""; + try_reconnect = FALSE; +@@ -857,6 +858,7 @@ bool login_proxy_failed(struct login_proxy *proxy, struct event *event, + + if (type != LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED && + type != LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED && ++ type != LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED && + type != LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL) + e_error(event, "%s%s", log_prefix, reason); + else if (proxy->client->set->auth_verbose) +diff --git a/src/login-common/login-proxy.h b/src/login-common/login-proxy.h +index 65895f3d75..736c2eb586 100644 +--- a/src/login-common/login-proxy.h ++++ b/src/login-common/login-proxy.h +@@ -35,6 +35,9 @@ enum login_proxy_failure_type { + /* Authentication failed to backend. The LOGIN/AUTH command reply was + already sent to the client. */ + LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED, ++ /* Authentication failed to backend. The LOGIN/AUTH command reply was ++ NOT sent to the client yet. */ ++ LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED, + /* Authentication failed with a temporary failure code. Attempting it + again might work. */ + LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL, +diff --git a/src/pop3-login/pop3-proxy.c b/src/pop3-login/pop3-proxy.c +index f8010d3d12..ad7786903b 100644 +--- a/src/pop3-login/pop3-proxy.c ++++ b/src/pop3-login/pop3-proxy.c +@@ -368,6 +368,7 @@ pop3_proxy_send_failure_reply(struct client *client, + break; + case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG: + case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED: + client_send_reply(client, POP3_CMD_REPLY_ERROR, + LOGIN_PROXY_FAILURE_MSG); + break; +diff --git a/src/submission-login/submission-proxy.c b/src/submission-login/submission-proxy.c +index 6f2c7f436a..f86845dcbc 100644 +--- a/src/submission-login/submission-proxy.c ++++ b/src/submission-login/submission-proxy.c +@@ -752,6 +752,7 @@ submission_proxy_send_failure_reply(struct submission_client *subm_client, + case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG: + case LOGIN_PROXY_FAILURE_TYPE_PROTOCOL: + case LOGIN_PROXY_FAILURE_TYPE_AUTH_REDIRECT: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED: + i_assert(cmd != NULL); + subm_client->pending_auth = NULL; + smtp_server_reply(cmd, 454, "4.7.0", LOGIN_PROXY_FAILURE_MSG); +-- +2.47.3 + diff --git a/patches/0001-login-common-Fix-crash-when-XCLIENT-FORWARD-base64-c.patch b/patches/0001-login-common-Fix-crash-when-XCLIENT-FORWARD-base64-c.patch new file mode 100644 index 0000000..9291f25 --- /dev/null +++ b/patches/0001-login-common-Fix-crash-when-XCLIENT-FORWARD-base64-c.patch @@ -0,0 +1,42 @@ +From 7c874f2d6461e80ff8f7111fa36a7551747129f1 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sun, 3 May 2026 16:20:00 +0000 +Subject: [PATCH 1/2] login-common: Fix crash when XCLIENT FORWARD= base64 + contains NUL byte + +Sending "XCLIENT FORWARD=AA==" (base64 for a single NUL byte) followed +by a regular login crashed pop3-login (and other login services) with: + + Panic: file ../../src/lib/array.h: line 275 (array_idx_i): + assertion failed: (idx < array->buffer->used / array->element_size) + +p_strsplit_tabescaped() truncates at the first NUL, so an all-NUL +payload produced an empty fields list. forward_fields was still +created (but empty), and the later array_front() call in +sasl_server_auth_begin() then panicked on the empty array. + +Reject empty payloads and payloads containing NUL bytes during base64 +decode, so the array is never created in this case. +--- + src/login-common/client-common.c | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/src/login-common/client-common.c b/src/login-common/client-common.c +index eeb9d6da05..00e4f9abbf 100644 +--- a/src/login-common/client-common.c ++++ b/src/login-common/client-common.c +@@ -988,6 +988,11 @@ bool client_forward_decode_base64(struct client *client, const char *value) + string_t *str = t_str_new(MAX_BASE64_DECODED_SIZE(value_len)); + if (base64_decode(value, value_len, str) < 0) + return FALSE; ++ /* Embedded NUL would yield a created-but-empty array, panicking later ++ in array_front() during sasl_server_auth_begin(). */ ++ if (str_len(str) == 0 || ++ memchr(str_data(str), '\0', str_len(str)) != NULL) ++ return FALSE; + + char **_fields = p_strsplit_tabescaped(client->preproxy_pool, + str_c(str)); +-- +2.47.3 + diff --git a/patches/0001-login-common-client-common-Add-client_disconnect-vfu.patch b/patches/0001-login-common-client-common-Add-client_disconnect-vfu.patch new file mode 100644 index 0000000..f0bf5c1 --- /dev/null +++ b/patches/0001-login-common-client-common-Add-client_disconnect-vfu.patch @@ -0,0 +1,36 @@ +From 275331da2aca21fb75483f226e72b0890d43ede4 Mon Sep 17 00:00:00 2001 +From: Stephan Bosch +Date: Fri, 28 Nov 2025 03:32:36 +0100 +Subject: [PATCH 1/5] login-common: client-common - Add client_disconnect() + vfunc + +--- + src/login-common/client-common.c | 2 ++ + src/login-common/client-common.h | 1 + + 2 files changed, 3 insertions(+) + +Index: trixie/src/login-common/client-common.c +=================================================================== +--- trixie.orig/src/login-common/client-common.c ++++ trixie/src/login-common/client-common.c +@@ -433,6 +433,8 @@ void client_disconnect(struct client *cl + if (!client->login_success) { + bool unref = FALSE; + ++ if (client->v.disconnect != NULL) ++ client->v.disconnect(client, reason); + io_remove(&client->io); + ssl_iostream_destroy(&client->ssl_iostream); + if (client->iostream_fd_proxy != NULL) { +Index: trixie/src/login-common/client-common.h +=================================================================== +--- trixie.orig/src/login-common/client-common.h ++++ trixie/src/login-common/client-common.h +@@ -122,6 +122,7 @@ struct client_auth_reply { + struct client_vfuncs { + struct client *(*alloc)(pool_t pool); + int (*create)(struct client *client); ++ void (*disconnect)(struct client *client, const char *reason); + void (*destroy)(struct client *client); + int (*reload_config)(struct client *client, const char **error_r); + void (*notify_auth_ready)(struct client *client); diff --git a/patches/0001-login-common-login-Add-proxy_dest_connection_limit-e.patch b/patches/0001-login-common-login-Add-proxy_dest_connection_limit-e.patch new file mode 100644 index 0000000..c056df0 --- /dev/null +++ b/patches/0001-login-common-login-Add-proxy_dest_connection_limit-e.patch @@ -0,0 +1,134 @@ +From db0835c84bf754a260ce0d3dca1d69eb90f87518 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 3 Dec 2025 12:06:42 +0200 +Subject: [PATCH] login-common, *-login: Add proxy_dest_connection_limit + error_code to proxy_session_finished + +If IMAP backend returns with [LIMIT] or POP3 backend returns with [IN-USE], +use this error code rather than the generic proxy_dest_auth_failed. +Error messages are also updated. +--- + src/imap-login/imap-proxy.c | 9 +++++++++ + src/login-common/client-common.c | 4 ++++ + src/login-common/login-proxy.c | 2 ++ + src/login-common/login-proxy.h | 3 +++ + src/pop3-login/pop3-proxy.c | 3 +++ + src/submission-login/submission-proxy.c | 1 + + 6 files changed, 22 insertions(+) + +Index: dovecot/src/imap-login/imap-proxy.c +=================================================================== +--- dovecot.orig/src/imap-login/imap-proxy.c ++++ dovecot/src/imap-login/imap-proxy.c +@@ -284,6 +284,12 @@ static bool auth_resp_code_is_serverbug( + strlen(IMAP_RESP_CODE_SERVERBUG"]")) == 0; + } + ++static bool auth_resp_code_is_limit(const char *resp) ++{ ++ return strncasecmp(resp, IMAP_RESP_CODE_LIMIT"]", ++ strlen(IMAP_RESP_CODE_LIMIT"]")) == 0; ++} ++ + static bool + auth_resp_code_parse_referral(struct client *client, const char *resp, + const char **userhostport_r) +@@ -448,6 +454,8 @@ int imap_proxy_parse_line(struct client + else if (auth_resp_code_is_serverbug(line + 4)) + failure_type = LOGIN_PROXY_FAILURE_TYPE_REMOTE; + else { ++ if (auth_resp_code_is_limit(line + 4)) ++ failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED; + client_send_raw(client, t_strconcat( + imap_client->cmd_tag, " ", line, "\r\n", NULL)); + } +@@ -587,6 +595,7 @@ imap_proxy_send_failure_reply(struct ima + imap_client->cmd_tag, " NO ", reason, "\r\n", NULL)); + break; + case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED: + /* reply was already sent */ + break; + } +Index: dovecot/src/login-common/client-common.c +=================================================================== +--- dovecot.orig/src/login-common/client-common.c ++++ dovecot/src/login-common/client-common.c +@@ -1397,6 +1397,10 @@ bool client_get_extra_disconnect_reason( + event_reason = "redirected"; + last_reason = "redirected"; + break; ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED: ++ event_reason = "connection_limit"; ++ last_reason = "connection limit reached"; ++ break; + default: + i_unreached(); + } +Index: dovecot/src/login-common/login-proxy.c +=================================================================== +--- dovecot.orig/src/login-common/login-proxy.c ++++ dovecot/src/login-common/login-proxy.c +@@ -830,6 +830,7 @@ bool login_proxy_failed(struct login_pro + log_prefix = "Remote server sent invalid input: "; + break; + case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED: + log_prefix = ""; + try_reconnect = FALSE; + break; +@@ -855,6 +856,7 @@ bool login_proxy_failed(struct login_pro + } + + if (type != LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED && ++ type != LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED && + type != LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL) + e_error(event, "%s%s", log_prefix, reason); + else if (proxy->client->set->auth_verbose) +Index: dovecot/src/login-common/login-proxy.h +=================================================================== +--- dovecot.orig/src/login-common/login-proxy.h ++++ dovecot/src/login-common/login-proxy.h +@@ -41,6 +41,9 @@ enum login_proxy_failure_type { + /* Authentication requests connecting to another host. The reason + string contains the host (and optionally :port). */ + LOGIN_PROXY_FAILURE_TYPE_AUTH_REDIRECT, ++ /* Authentication failed because user has reached some limit. ++ The LOGIN/AUTH command reply was already sent to the client. */ ++ LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED, + }; + + struct login_proxy_settings { +Index: dovecot/src/pop3-login/pop3-proxy.c +=================================================================== +--- dovecot.orig/src/pop3-login/pop3-proxy.c ++++ dovecot/src/pop3-login/pop3-proxy.c +@@ -333,6 +333,8 @@ int pop3_proxy_parse_line(struct client + } else if (pop3_proxy_parse_referral(client, line + 5, &line)) { + failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_REDIRECT; + } else { ++ if (str_begins_with(line, "-ERR [IN-USE]")) ++ failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED; + client_send_raw(client, t_strconcat(line, "\r\n", NULL)); + line += 5; + } +@@ -374,6 +376,7 @@ pop3_proxy_send_failure_reply(struct cli + client_send_reply(client, POP3_CMD_REPLY_ERROR, reason); + break; + case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED: + /* reply was already sent */ + break; + } +Index: dovecot/src/submission-login/submission-proxy.c +=================================================================== +--- dovecot.orig/src/submission-login/submission-proxy.c ++++ dovecot/src/submission-login/submission-proxy.c +@@ -750,6 +750,7 @@ submission_proxy_send_failure_reply(stru + smtp_server_reply_submit(subm_client->proxy_reply); + break; + case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED: + /* reply was already sent */ + i_assert(cmd == NULL); + break; diff --git a/patches/0001-managesieve-login-Adjust-to-core-s-login_proxy_failu.patch b/patches/0001-managesieve-login-Adjust-to-core-s-login_proxy_failu.patch new file mode 100644 index 0000000..53d4a57 --- /dev/null +++ b/patches/0001-managesieve-login-Adjust-to-core-s-login_proxy_failu.patch @@ -0,0 +1,30 @@ +Index: dovecot/pigeonhole/src/managesieve-login/managesieve-proxy.c +=================================================================== +--- dovecot.orig/pigeonhole/src/managesieve-login/managesieve-proxy.c ++++ dovecot/pigeonhole/src/managesieve-login/managesieve-proxy.c +@@ -658,7 +658,7 @@ int managesieve_proxy_parse_line(struct + &reason)) + failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_REDIRECT; + else { +- failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH; ++ failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED; + client_send_no(client, AUTH_FAILED_MSG); + } + +@@ -702,6 +702,7 @@ managesieve_proxy_send_failure_reply(str + break; + case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG: + case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED: + client_send_reply_code(client, MANAGESIEVE_CMD_REPLY_NO, + NULL, LOGIN_PROXY_FAILURE_MSG); + break; +@@ -709,7 +710,7 @@ managesieve_proxy_send_failure_reply(str + client_send_reply_code(client, MANAGESIEVE_CMD_REPLY_NO, + "TRYLATER", reason); + break; +- case LOGIN_PROXY_FAILURE_TYPE_AUTH: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: + /* reply was already sent */ + break; + } diff --git a/patches/0001-managesieve-login-Support-LOGIN_PROXY_FAILURE_TYPE_A.patch b/patches/0001-managesieve-login-Support-LOGIN_PROXY_FAILURE_TYPE_A.patch new file mode 100644 index 0000000..78c4e12 --- /dev/null +++ b/patches/0001-managesieve-login-Support-LOGIN_PROXY_FAILURE_TYPE_A.patch @@ -0,0 +1,12 @@ +Index: dovecot/pigeonhole/src/managesieve-login/managesieve-proxy.c +=================================================================== +--- dovecot.orig/pigeonhole/src/managesieve-login/managesieve-proxy.c ++++ dovecot/pigeonhole/src/managesieve-login/managesieve-proxy.c +@@ -711,6 +711,7 @@ managesieve_proxy_send_failure_reply(str + "TRYLATER", reason); + break; + case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED: ++ case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED: + /* reply was already sent */ + break; + } diff --git a/patches/0001-submission-Fix-settings-leak-on-error-paths-in-clien.patch b/patches/0001-submission-Fix-settings-leak-on-error-paths-in-clien.patch new file mode 100644 index 0000000..de0fc9a --- /dev/null +++ b/patches/0001-submission-Fix-settings-leak-on-error-paths-in-clien.patch @@ -0,0 +1,33 @@ +From 53abb9f06e9d81ea41e30c6552d0ff0047e41e9d Mon Sep 17 00:00:00 2001 +From: Markus Valentin +Date: Thu, 23 Apr 2026 12:53:07 +0200 +Subject: [PATCH 1/2] submission: Fix settings leak on error paths in + client_create_from_input() + +--- + src/submission/main.c | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/src/submission/main.c b/src/submission/main.c +index 00e6642510..3b110f4f4a 100644 +--- a/src/submission/main.c ++++ b/src/submission/main.c +@@ -208,6 +208,7 @@ client_create_from_input(const struct mail_storage_service_input *input, + "(submission_relay_host is unset)"; + send_error(fd_out, event, set->hostname, + "4.3.5", MAIL_ERRSTR_CRITICAL_MSG); ++ settings_free(set); + mail_user_deinit(&mail_user); + event_unref(&event); + return -1; +@@ -216,6 +217,7 @@ client_create_from_input(const struct mail_storage_service_input *input, + if (ret < 0) { + send_error(fd_out, event, my_hostname, + "4.7.0", MAIL_ERRSTR_CRITICAL_MSG); ++ settings_free(set); + mail_user_deinit(&mail_user); + event_unref(&event); + return -1; +-- +2.47.3 + diff --git a/patches/0001-trash-Use-mailbox-event-in-trash_try_mailbox-for-set.patch b/patches/0001-trash-Use-mailbox-event-in-trash_try_mailbox-for-set.patch new file mode 100644 index 0000000..10683b0 --- /dev/null +++ b/patches/0001-trash-Use-mailbox-event-in-trash_try_mailbox-for-set.patch @@ -0,0 +1,63 @@ +From 06af53902479572fc96f04b4372fdabb9d01996b Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Fri, 9 Jan 2026 13:31:42 +0200 +Subject: [PATCH] trash: Use mailbox event in trash_try_mailbox() for settings +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1127029 + +--- + src/plugins/trash/trash-plugin.c | 18 ++++++++++-------- + 1 file changed, 10 insertions(+), 8 deletions(-) + +diff --git a/src/plugins/trash/trash-plugin.c b/src/plugins/trash/trash-plugin.c +index 827ba91ec5..7865de2ef6 100644 +--- a/src/plugins/trash/trash-plugin.c ++++ b/src/plugins/trash/trash-plugin.c +@@ -570,14 +570,13 @@ static int trash_mailbox_priority_cmp(const struct trash_mailbox *t1, + return strcmp(t1->name, t2->name); + } + +-static int trash_try_mailbox(struct mail_namespace *ns, const char *box_name, ++static int trash_try_mailbox(struct mail_namespace *ns, struct event *mbox_event, + const char **error_r) + { + struct trash_user *tuser = TRASH_USER_CONTEXT_REQUIRE(ns->user); + const struct trash_settings *trash_set; +- if (settings_try_get_filter(ns->list->event, "mailbox", box_name, +- &trash_setting_parser_info, 0, +- &trash_set, error_r) < 0) ++ if (settings_get(mbox_event, &trash_setting_parser_info, 0, ++ &trash_set, error_r) < 0) + return -1; + unsigned int trash_priority = trash_set->trash_priority; + settings_free(trash_set); +@@ -586,9 +585,8 @@ static int trash_try_mailbox(struct mail_namespace *ns, const char *box_name, + return 0; + + const struct mailbox_settings *box_set; +- if (settings_try_get_filter(ns->list->event, "mailbox", box_name, +- &mailbox_setting_parser_info, 0, +- &box_set, error_r) < 0) ++ if (settings_get(mbox_event, &mailbox_setting_parser_info, 0, ++ &box_set, error_r) < 0) + return -1; + + const char *vname = +@@ -618,10 +616,14 @@ static int trash_find_mailboxes(struct mail_user *user) + continue; + + array_foreach_elem(&ns->set->mailboxes, box_name) { +- if (trash_try_mailbox(ns, box_name, &error) < 0) { ++ struct event *mbox_event = ++ mail_storage_mailbox_create_event(ns->list->event, ns->list, box_name); ++ if (trash_try_mailbox(ns, mbox_event, &error) < 0) { + user->error = p_strdup(user->pool, error); ++ event_unref(&mbox_event); + return -1; + } ++ event_unref(&mbox_event); + } + } + +-- +2.47.3 + diff --git a/patches/0002-auth-Fix-prefixing-forward_fields-without-a-value-fr.patch b/patches/0002-auth-Fix-prefixing-forward_fields-without-a-value-fr.patch new file mode 100644 index 0000000..a69e3cc --- /dev/null +++ b/patches/0002-auth-Fix-prefixing-forward_fields-without-a-value-fr.patch @@ -0,0 +1,30 @@ +From ed04e392f97a523dd7bc75161af280df6cf3ce6f Mon Sep 17 00:00:00 2001 +From: Markus Valentin +Date: Thu, 23 Apr 2026 13:07:08 +0200 +Subject: [PATCH 2/2] auth: Fix prefixing forward_fields without a value from + client + +Bare tokens (without '=') were not prefixed, only key=value pairs were. +In practice this affected forward_fields, where a bare token such as +'nopassword' would land in extra_fields unprefixed instead of as +'forward_nopassword', allowing injection of internal auth control fields. +--- + src/auth/auth-fields.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/auth/auth-fields.c b/src/auth/auth-fields.c +index 8b92836588..6e1ccbbcab 100644 +--- a/src/auth/auth-fields.c ++++ b/src/auth/auth-fields.c +@@ -125,7 +125,7 @@ static void auth_fields_import_prefixed_args(struct auth_fields *fields, + for (; *args != NULL; args++) { + value = strchr(*args, '='); + if (value == NULL) { +- key = *args; ++ key = *prefix != '\0' ? t_strconcat(prefix, *args, NULL) : *args; + } else { + key = t_strdup_until(*args, value++); + if (*prefix != '\0') +-- +2.47.3 + diff --git a/patches/0002-auth-db-oauth2-Fix-scope-check-to-require-all-config.patch b/patches/0002-auth-db-oauth2-Fix-scope-check-to-require-all-config.patch new file mode 100644 index 0000000..93d0c1f --- /dev/null +++ b/patches/0002-auth-db-oauth2-Fix-scope-check-to-require-all-config.patch @@ -0,0 +1,49 @@ +From 5b894458088293ffaef32d028af81a70506b7f20 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 3 Jun 2026 12:56:20 +0000 +Subject: [PATCH 2/4] auth: db-oauth2: Fix scope check to require all + configured scopes + +Switch token-in-scope check to AND semantics: all configured scopes +must be present in the token. Behaviour now matches the JWT path. +--- + src/auth/db-oauth2.c | 17 ++++++++++------- + 1 file changed, 10 insertions(+), 7 deletions(-) + +Index: trixie/src/auth/db-oauth2.c +=================================================================== +--- trixie.orig/src/auth/db-oauth2.c ++++ trixie/src/auth/db-oauth2.c +@@ -556,22 +556,25 @@ db_oauth2_token_in_scope(struct db_oauth + if (array_is_empty(&req->db->set->scope)) + return TRUE; + +- bool found = FALSE; + const char *value = auth_fields_find(req->fields, "scope"); + bool has_scope = value != NULL; + if (!has_scope) + value = auth_fields_find(req->fields, "aud"); + e_debug(authdb_event(req->auth_request), +- "Token scope(s): %s", +- value); +- if (value != NULL) { +- const char *wanted_scope; ++ "Token scope(s): %s", value); ++ ++ bool found = FALSE; ++ if (value != NULL && *value != '\0') { + const char *const *entries = has_scope ? + t_strsplit_spaces(value, " ") : + t_strsplit_tabescaped(value); +- array_foreach_elem(&req->db->set->scope, wanted_scope) { +- if ((found = str_array_find(entries, wanted_scope))) ++ const char *wanted; ++ found = TRUE; ++ array_foreach_elem(&req->db->set->scope, wanted) { ++ if (!str_array_find(entries, wanted)) { ++ found = FALSE; + break; ++ } + } + } + if (!found) { diff --git a/patches/0002-doveadm-Avoid-leaking-doveadm_password-or-doveadm_ap.patch b/patches/0002-doveadm-Avoid-leaking-doveadm_password-or-doveadm_ap.patch new file mode 100644 index 0000000..bb6a17b --- /dev/null +++ b/patches/0002-doveadm-Avoid-leaking-doveadm_password-or-doveadm_ap.patch @@ -0,0 +1,49 @@ +From 3c847240b1c0afc52ee570f45409f094c546a3a0 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 28 Apr 2026 01:51:01 +0300 +Subject: [PATCH 2/2] doveadm: Avoid leaking doveadm_password or + doveadm_api_key lengths + +--- + src/doveadm/client-connection-http.c | 4 ++-- + src/doveadm/client-connection-tcp.c | 2 +- + 2 files changed, 3 insertions(+), 3 deletions(-) + +diff --git a/src/doveadm/client-connection-http.c b/src/doveadm/client-connection-http.c +index dfbc3528d7..f083ac6e14 100644 +--- a/src/doveadm/client-connection-http.c ++++ b/src/doveadm/client-connection-http.c +@@ -973,7 +973,7 @@ doveadm_http_server_auth_basic(struct client_request_http *req, + b64_value = t_base64_encode_str(0, UINT_MAX, value); + + if (creds->data != NULL && +- str_equals_timing_almost_safe(str_c(b64_value), creds->data)) ++ str_equals_hash_timing_safe(str_c(b64_value), creds->data)) + return TRUE; + + e_error(conn->conn.event, +@@ -999,7 +999,7 @@ doveadm_http_server_auth_api_key(struct client_request_http *req, + + b64_value = t_base64_encode_str(0, UINT_MAX, set->doveadm_api_key); + if (creds->data != NULL && +- str_equals_timing_almost_safe(creds->data, str_c(b64_value))) ++ str_equals_hash_timing_safe(creds->data, str_c(b64_value))) + return TRUE; + + e_error(conn->conn.event, +diff --git a/src/doveadm/client-connection-tcp.c b/src/doveadm/client-connection-tcp.c +index cb16f7214b..b374028c97 100644 +--- a/src/doveadm/client-connection-tcp.c ++++ b/src/doveadm/client-connection-tcp.c +@@ -400,7 +400,7 @@ client_connection_tcp_authenticate(struct client_connection_tcp *conn) + return -1; + } + pass = t_strndup(data + 9, size - 9); +- if (!str_equals_timing_almost_safe(pass, set->doveadm_password)) { ++ if (!str_equals_hash_timing_safe(pass, set->doveadm_password)) { + e_error(conn->conn.event, + "doveadm client authenticated with wrong password"); + return -1; +-- +2.47.3 + diff --git a/patches/0002-doveadm-compress-connect-Don-t-hang-when-server-reje.patch b/patches/0002-doveadm-compress-connect-Don-t-hang-when-server-reje.patch new file mode 100644 index 0000000..4840ba6 --- /dev/null +++ b/patches/0002-doveadm-compress-connect-Don-t-hang-when-server-reje.patch @@ -0,0 +1,120 @@ +From 4a731e5de4191134e27e14c62a42ae7201d26e26 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 6 Jul 2026 07:17:42 +0000 +Subject: [PATCH 2/3] doveadm: compress-connect - Don't hang when server + rejects COMPRESS + +After sending a COMPRESS command the client stopped reading stdin and +waited for the server's reply, but it only resumed on an "OK Begin +compression" reply. If the server rejected COMPRESS (e.g. a NO reply for +an unsupported mechanism), compress_waiting was never cleared: the client +waited forever for an OK that never came while the server waited for the +next command, deadlocking the session. + +Remember the COMPRESS command tag and treat any tagged reply to it that +isn't the compression-start reply as a rejection, resuming the pipelined +input. +--- + src/doveadm/doveadm-compress.c | 34 +++++++++++++++++++++++++++++++++- + 1 file changed, 33 insertions(+), 1 deletion(-) + +Index: trixie/src/doveadm/doveadm-compress.c +=================================================================== +--- trixie.orig/src/doveadm/doveadm-compress.c ++++ trixie/src/doveadm/doveadm-compress.c +@@ -20,6 +20,7 @@ enum server_input_line_type { + SERVER_INPUT_LINE_TYPE_DEFAULT, + SERVER_INPUT_LINE_TYPE_STARTTLS, + SERVER_INPUT_LINE_TYPE_COMPRESS, ++ SERVER_INPUT_LINE_TYPE_COMPRESS_FAILED, + }; + + struct client { +@@ -32,6 +33,7 @@ struct client { + struct ssl_iostream *ssl_iostream; + const struct compression_handler *handler; + char *algorithm; ++ char *compress_tag; + bool tls; + bool compressed; + bool compress_waiting; +@@ -111,17 +113,23 @@ cmd_dump_imap_compress(struct doveadm_cm + static bool + client_input_get_compress_algorithm(struct client *client, const char *line) + { +- const char *algorithm; ++ const char *algorithm, *tag = line; ++ size_t tag_len; + + /* skip tag */ + while (*line != ' ' && *line != '\0') + line++; ++ tag_len = line - tag; + if (!str_begins_icase(line, " COMPRESS ", &algorithm)) + return FALSE; + + if (compression_lookup_handler(t_str_lcase(algorithm), + &client->handler) <= 0) + i_fatal("Unsupported compression mechanism: %s", algorithm); ++ /* Remember the tag so we can tell whether the server accepted or ++ rejected this COMPRESS command. */ ++ i_free(client->compress_tag); ++ client->compress_tag = i_strndup(tag, tag_len); + return TRUE; + } + +@@ -190,6 +198,13 @@ static bool server_input_is_compress_rep + return str_begins_with(line, " OK Begin compression"); + } + ++static bool server_input_is_reply_to_tag(const char *line, const char *tag) ++{ ++ size_t tag_len = strlen(tag); ++ ++ return strncmp(line, tag, tag_len) == 0 && line[tag_len] == ' '; ++} ++ + static enum server_input_line_type + server_input_line_type(struct client *client, const char *line) + { +@@ -197,6 +212,13 @@ server_input_line_type(struct client *cl + return SERVER_INPUT_LINE_TYPE_STARTTLS; + if (!client->compressed && server_input_is_compress_reply(line)) + return SERVER_INPUT_LINE_TYPE_COMPRESS; ++ /* The server can reject COMPRESS (e.g. an unsupported mechanism). If we ++ are waiting for its reply, a tagged reply that isn't the "OK Begin ++ compression" above means the command failed - resume sending the ++ pipelined input instead of waiting forever. */ ++ if (client->compress_waiting && client->compress_tag != NULL && ++ server_input_is_reply_to_tag(line, client->compress_tag)) ++ return SERVER_INPUT_LINE_TYPE_COMPRESS_FAILED; + return SERVER_INPUT_LINE_TYPE_DEFAULT; + } + +@@ -269,9 +291,18 @@ static void server_input(struct client * + client->output = output; + client->compressed = TRUE; + client->compress_waiting = FALSE; ++ i_free_and_null(client->compress_tag); + i_stream_set_input_pending(client->stdin_input, TRUE); + break; + } ++ case SERVER_INPUT_LINE_TYPE_COMPRESS_FAILED: ++ /* Server rejected COMPRESS. Resume sending input. */ ++ e_info(client->event, ""); ++ client->handler = NULL; ++ client->compress_waiting = FALSE; ++ i_free_and_null(client->compress_tag); ++ i_stream_set_input_pending(client->stdin_input, TRUE); ++ break; + case SERVER_INPUT_LINE_TYPE_DEFAULT: + break; + } +@@ -329,6 +360,7 @@ static void cmd_compress_connect(struct + ssl_iostream_destroy(&client.ssl_iostream); + i_stream_unref(&client.stdin_input); + i_stream_unref(&client.input); ++ i_free(client.compress_tag); + o_stream_unref(&client.output); + event_unref(&client.event); + if (close(fd) < 0) diff --git a/patches/0002-dsync-Fix-escaping-mail-or-attribute-value-that-begi.patch b/patches/0002-dsync-Fix-escaping-mail-or-attribute-value-that-begi.patch new file mode 100644 index 0000000..05b5a01 --- /dev/null +++ b/patches/0002-dsync-Fix-escaping-mail-or-attribute-value-that-begi.patch @@ -0,0 +1,37 @@ +From a8b185e828d1a67ab66efd5c4a7d689e5eef0025 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 24 Mar 2026 12:03:08 +0200 +Subject: [PATCH 2/2] dsync: Fix escaping mail or attribute value that begins + with a "." line + +Such a mail or attribute would have escaped the value parameter and the rest +of the value would have been processed as dsync stream commands. +--- + src/doveadm/dsync/dsync-ibc-stream.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/doveadm/dsync/dsync-ibc-stream.c b/src/doveadm/dsync/dsync-ibc-stream.c +index 0b3038201d..390c9a50c0 100644 +--- a/src/doveadm/dsync/dsync-ibc-stream.c ++++ b/src/doveadm/dsync/dsync-ibc-stream.c +@@ -1533,7 +1533,7 @@ dsync_ibc_stream_send_mailbox_attribute(struct dsync_ibc *_ibc, + dsync_ibc_stream_send_string(ibc, str); + + if (attr->value_stream != NULL) { +- ibc->value_output_last = '\0'; ++ ibc->value_output_last = '\n'; + ibc->value_output = attr->value_stream; + i_stream_ref(ibc->value_output); + (void)dsync_ibc_stream_send_value_stream(ibc); +@@ -1913,7 +1913,7 @@ dsync_ibc_stream_send_mail(struct dsync_ibc *_ibc, + dsync_ibc_stream_send_string(ibc, str); + + if (mail->input != NULL) { +- ibc->value_output_last = '\0'; ++ ibc->value_output_last = '\n'; + ibc->value_output = mail->input; + i_stream_ref(ibc->value_output); + (void)dsync_ibc_stream_send_value_stream(ibc); +-- +2.47.3 + diff --git a/patches/0002-imap-Recreate-multiplex-ostream-side-channel-after-u.patch b/patches/0002-imap-Recreate-multiplex-ostream-side-channel-after-u.patch new file mode 100644 index 0000000..7590d91 --- /dev/null +++ b/patches/0002-imap-Recreate-multiplex-ostream-side-channel-after-u.patch @@ -0,0 +1,33 @@ +From d4fc8475f3b9cf8debbf9427d7bb8e9bbecb07ba Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 1 May 2026 17:40:17 +0000 +Subject: [PATCH 2/5] imap: Recreate multiplex ostream side channel after + unhibernation + +This will be needed by the following changes to send dict_reset commands via +the side channel. +--- + src/imap/imap-state.c | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/src/imap/imap-state.c b/src/imap/imap-state.c +index 3736296bea..fbd383d9de 100644 +--- a/src/imap/imap-state.c ++++ b/src/imap/imap-state.c +@@ -723,6 +723,13 @@ import_state_compress(struct client *client, const unsigned char *data, + *error_r = t_strdup_printf("Unknown COMPRESS handler %s", name); + return 0; + } ++ /* Recreate the side channel that cmd-compress would have created in ++ the previous (now-exited) imap process. It is used to forward ++ dict_reset commands back to the imap-login proxy after every tagged ++ reply. */ ++ if (client->multiplex_output != NULL && ++ client->side_channel_output == NULL) ++ client_create_side_channel_output(client); + return p - data; + } + +-- +2.47.3 + diff --git a/patches/0002-imap-Stream-BODYSTRUCTURE-to-client-without-ostream-.patch b/patches/0002-imap-Stream-BODYSTRUCTURE-to-client-without-ostream-.patch new file mode 100644 index 0000000..84dd38a --- /dev/null +++ b/patches/0002-imap-Stream-BODYSTRUCTURE-to-client-without-ostream-.patch @@ -0,0 +1,79 @@ +From 480e9dfb6140f51992fdf83a9e9418e1dcf742d2 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 17 Apr 2026 16:55:05 +0200 +Subject: [PATCH 02/14] imap: Stream BODYSTRUCTURE to client without ostream + memory duplication + +For huge BODYSTRUCTURE responses (messages with many MIME parts), +o_stream_send_str() caused the ostream to copy the entire string into +its ring buffer when the stream was corked, doubling memory usage. + +Use the same o_stream_set_max_buffer_size(0) + o_stream_send_istream() +pattern as fetch_stream_continue() so the ostream buffer stays at 0 +bytes and the I/O loop handles flow control via WAIT_OUTPUT. + +Co-Authored-By: Claude Sonnet 4.6 +--- + src/imap/imap-fetch.c | 37 +++++++++++++++++++++++++++++++++---- + 1 file changed, 33 insertions(+), 4 deletions(-) + +diff --git a/src/imap/imap-fetch.c b/src/imap/imap-fetch.c +index ca4b3216ce..b4a8a465d7 100644 +--- a/src/imap/imap-fetch.c ++++ b/src/imap/imap-fetch.c +@@ -732,6 +732,34 @@ static bool fetch_body_init(struct imap_fetch_init_context *ctx) + return imap_fetch_body_section_init(ctx); + } + ++static int fetch_bodystructure_stream_continue(struct imap_fetch_context *ctx) ++{ ++ struct imap_fetch_state *state = &ctx->state; ++ enum ostream_send_istream_result res; ++ ++ o_stream_set_max_buffer_size(ctx->client->output, 0); ++ res = o_stream_send_istream(ctx->client->output, state->cur_input); ++ o_stream_set_max_buffer_size(ctx->client->output, SIZE_MAX); ++ ++ switch (res) { ++ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED: ++ i_stream_unref(&state->cur_input); ++ state->cont_handler = NULL; ++ if (o_stream_send(ctx->client->output, ")", 1) < 0) ++ return -1; ++ return 1; ++ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT: ++ i_unreached(); ++ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT: ++ return 0; ++ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT: ++ i_unreached(); ++ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT: ++ return -1; ++ } ++ i_unreached(); ++} ++ + static int fetch_bodystructure(struct imap_fetch_context *ctx, + struct mail *mail, void *context ATTR_UNUSED) + { +@@ -748,12 +776,13 @@ static int fetch_bodystructure(struct imap_fetch_context *ctx, + return -1; + } + +- if (o_stream_send(ctx->client->output, "BODYSTRUCTURE (", 15) < 0 || +- o_stream_send_str(ctx->client->output, bodystructure) < 0 || +- o_stream_send(ctx->client->output, ")", 1) < 0) ++ if (o_stream_send(ctx->client->output, "BODYSTRUCTURE (", 15) < 0) + return -1; + +- return 1; ++ ctx->state.cur_input = ++ i_stream_create_from_data(bodystructure, strlen(bodystructure)); ++ ctx->state.cont_handler = fetch_bodystructure_stream_continue; ++ return fetch_bodystructure_stream_continue(ctx); + } + + static bool fetch_bodystructure_init(struct imap_fetch_init_context *ctx) +-- +2.47.3 + diff --git a/patches/0002-imap-hibernate-Use-imap-parser-API-for-parsing-DONE-.patch b/patches/0002-imap-hibernate-Use-imap-parser-API-for-parsing-DONE-.patch new file mode 100644 index 0000000..945e040 --- /dev/null +++ b/patches/0002-imap-hibernate-Use-imap-parser-API-for-parsing-DONE-.patch @@ -0,0 +1,152 @@ +From fc1898a575fd4cf4c2a4b3e00da630825c0acf60 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 22 Apr 2026 12:58:56 +0300 +Subject: [PATCH 2/2] imap-hibernate: Use imap-parser API for parsing DONE + command + +Replace the ad-hoc DONE/IDLE tokenizer with imap-parser.h: use +imap_parser_read_tag() to read the tag and imap_parser_read_word() to +read the DONE and IDLE keywords. This delegates bounds checks and +character validation to the shared parser. +--- + src/imap-hibernate/imap-client.c | 103 ++++++++++++++++++------------- + 1 file changed, 59 insertions(+), 44 deletions(-) + +diff --git a/src/imap-hibernate/imap-client.c b/src/imap-hibernate/imap-client.c +index 2110fe8e8e..564ef44f4f 100644 +--- a/src/imap-hibernate/imap-client.c ++++ b/src/imap-hibernate/imap-client.c +@@ -21,6 +21,7 @@ + #include "master-service-settings.h" + #include "imap-keepalive.h" + #include "imap-master-connection.h" ++#include "imap-parser.h" + #include "imap-client.h" + + #include +@@ -334,64 +335,78 @@ static void imap_client_move_back(struct imap_client *client) + } + + static enum imap_client_input_state +-imap_client_input_parse(const unsigned char *data, size_t size, const char **tag_r) ++imap_client_input_parse_input(struct istream *input, struct imap_parser *parser, ++ const char **tag_r) + { +- const unsigned char *tag_start, *tag_end; +- ++ const unsigned char *rest; ++ size_t rest_size; ++ const char *word, *tag; + enum imap_client_input_state state = IMAP_CLIENT_INPUT_STATE_DONE_LF; ++ int ret; + +- /* skip over DONE[\r]\n */ +- if (i_memcasecmp(data, "DONE", I_MIN(size, 4)) != 0) +- return IMAP_CLIENT_INPUT_STATE_BAD; +- if (size <= 4) ++ /* read DONE */ ++ word = imap_parser_read_word(parser); ++ if (word == NULL) + return IMAP_CLIENT_INPUT_STATE_UNKNOWN; +- data += 4; size -= 4; ++ if (strcasecmp(word, "DONE") != 0) ++ return IMAP_CLIENT_INPUT_STATE_BAD; + +- if (data[0] == '\r') { ++ /* read [\r]\n after DONE */ ++ rest = i_stream_get_data(input, &rest_size); ++ if (rest_size == 0) ++ return IMAP_CLIENT_INPUT_STATE_UNKNOWN; ++ if (rest[0] == '\r') { + state = IMAP_CLIENT_INPUT_STATE_DONE_CRLF; +- data++; size--; ++ i_stream_skip(input, 1); ++ rest = i_stream_get_data(input, &rest_size); ++ if (rest_size == 0) ++ return IMAP_CLIENT_INPUT_STATE_UNKNOWN; + } +- if (size == 0) +- return IMAP_CLIENT_INPUT_STATE_UNKNOWN; +- if (data[0] != '\n') ++ if (rest[0] != '\n') + return IMAP_CLIENT_INPUT_STATE_BAD; +- data++; size--; +- if (size == 0) ++ i_stream_skip(input, 1); ++ ++ /* optionally followed by " IDLE[\r]\n" - checking this assumes ++ that the DONE and IDLE are sent in the same IP packet, otherwise ++ we'll unnecessarily recreate the imap process and immediately resume ++ IDLE there. if this becomes an issue we could add a small delay to ++ the imap process creation and wait for the IDLE command during it. */ ++ rest = i_stream_get_data(input, &rest_size); ++ if (rest_size == 0) + return state; + +- tag_start = data; +- +- /* skip over tag */ +- while(size > 0 && +- data[0] != ' ' && +- data[0] != '\r' && +- data[0] != '\t' && +- data[0] != '\0') { data++; size--; } +- +- tag_end = data; +- +- if (size == 0) ++ ret = imap_parser_read_tag(parser, &tag); ++ if (ret <= 0) + return state; +- if (data[0] != ' ') +- return IMAP_CLIENT_INPUT_STATE_BAD; +- data++; size--; +- +- /* skip over IDLE[\r]\n - checking this assumes that the DONE and IDLE +- are sent in the same IP packet, otherwise we'll unnecessarily +- recreate the imap process and immediately resume IDLE there. if this +- becomes an issue we could add a small delay to the imap process +- creation and wait for the IDLE command during it. */ +- if (size <= 4 || i_memcasecmp(data, "IDLE", 4) != 0) ++ ++ /* read IDLE */ ++ word = imap_parser_read_word(parser); ++ if (word == NULL || strcasecmp(word, "IDLE") != 0) + return state; +- data += 4; size -= 4; + +- if (data[0] == '\r') { +- data++; size--; +- } +- if (size == 1 && data[0] == '\n') { +- *tag_r = t_strdup_until(tag_start, tag_end); +- return IMAP_CLIENT_INPUT_STATE_DONEIDLE; ++ /* require [\r]\n and nothing else left */ ++ rest = i_stream_get_data(input, &rest_size); ++ if (rest_size > 0 && rest[0] == '\r') { ++ i_stream_skip(input, 1); ++ rest = i_stream_get_data(input, &rest_size); + } ++ if (rest_size != 1 || rest[0] != '\n') ++ return state; ++ ++ *tag_r = t_strdup(tag); ++ return IMAP_CLIENT_INPUT_STATE_DONEIDLE; ++} ++ ++static enum imap_client_input_state ++imap_client_input_parse(const unsigned char *data, size_t size, const char **tag_r) ++{ ++ struct istream *input = i_stream_create_from_data(data, size); ++ struct imap_parser *parser = ++ imap_parser_create(input, NULL, size, NULL); ++ enum imap_client_input_state state = ++ imap_client_input_parse_input(input, parser, tag_r); ++ imap_parser_unref(&parser); ++ i_stream_unref(&input); + return state; + } + +-- +2.47.3 + diff --git a/patches/0002-lda-Default-mail_home-HOME-environment-if-not-using-.patch b/patches/0002-lda-Default-mail_home-HOME-environment-if-not-using-.patch new file mode 100644 index 0000000..b5ac966 --- /dev/null +++ b/patches/0002-lda-Default-mail_home-HOME-environment-if-not-using-.patch @@ -0,0 +1,34 @@ +From 42864e4182c7bfe2931fdf888f3cb4ba77953dee Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 26 May 2025 09:45:56 +0300 +Subject: [PATCH] lda: Default mail_home=$HOME environment if not using userdb + lookup + +The previous code to do this was removed by +e57d5b9002f910c095ee5b55821395fcf1da016a +--- + src/lda/main.c | 5 +++++ + 1 file changed, 5 insertions(+) + +Index: dovecot/src/lda/main.c +=================================================================== +--- dovecot.orig/src/lda/main.c ++++ dovecot/src/lda/main.c +@@ -478,12 +478,17 @@ int main(int argc, char *argv[]) + "Couldn't lookup our username (uid=%s)", + dec2str(process_euid)); + } ++ struct settings_root *set_root = ++ master_service_get_settings_root(master_service); ++ settings_root_override(set_root, "mail_home", home, ++ SETTINGS_OVERRIDE_TYPE_DEFAULT); + } else { + i_fatal_status(EX_USAGE, + "destination user parameter (-d user) not given"); + } + struct master_service_settings_input set_input = { + .preserve_user = TRUE, ++ .preserve_home = TRUE, + }; + struct master_service_settings_output set_output; + if (master_service_settings_read(master_service, &set_input, diff --git a/patches/0002-lib-Replace-str_hash-strcase_hash-with-xxh64.patch b/patches/0002-lib-Replace-str_hash-strcase_hash-with-xxh64.patch new file mode 100644 index 0000000..74e5a3a --- /dev/null +++ b/patches/0002-lib-Replace-str_hash-strcase_hash-with-xxh64.patch @@ -0,0 +1,80 @@ +From bd11a6525eeb214541fa26450550f8b1bb4baec7 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 13 Apr 2026 12:33:51 +0200 +Subject: [PATCH 02/12] lib: Replace str_hash/strcase_hash with xxh64 + +Replaces the old ASU-derived shift-and-XOR hash with xxh64_to_32(), +which provides better distribution and avalanche properties. +strcase_hash feeds each i_toupper()'d byte through the streaming API. +--- + src/lib/hash.c | 41 +++++++++++------------------------------ + 1 file changed, 11 insertions(+), 30 deletions(-) + +diff --git a/src/lib/hash.c b/src/lib/hash.c +index aceef29b23..8f4cd7d9b4 100644 +--- a/src/lib/hash.c ++++ b/src/lib/hash.c +@@ -5,6 +5,7 @@ + #include "lib.h" + #include "hash.h" + #include "primes.h" ++#include "xxh64.h" + + #include + +@@ -519,42 +520,22 @@ void hash_table_copy(struct hash_table *dest, struct hash_table *src) + hash_table_thaw(dest); + } + +-/* a char* hash function from ASU -- from glib */ +-unsigned int ATTR_NO_SANITIZE_INTEGER +-str_hash(const char *p) ++unsigned int str_hash(const char *p) + { +- const unsigned char *s = (const unsigned char *)p; +- unsigned int g, h = 0; +- +- while (*s != '\0') { +- h = (h << 4) + *s; +- if ((g = h & 0xf0000000UL) != 0) { +- h = h ^ (g >> 24); +- h = h ^ g; +- } +- s++; +- } +- +- return h; ++ return xxh64_to_32(xxh64_data(p, strlen(p), 0)); + } + +-/* a char* hash function from ASU -- from glib */ +-unsigned int ATTR_NO_SANITIZE_INTEGER +-strcase_hash(const char *p) ++unsigned int strcase_hash(const char *p) + { +- const unsigned char *s = (const unsigned char *)p; +- unsigned int g, h = 0; ++ struct xxh64_context ctx; ++ unsigned char c; + +- while (*s != '\0') { +- h = (h << 4) + i_toupper(*s); +- if ((g = h & 0xf0000000UL) != 0) { +- h = h ^ (g >> 24); +- h = h ^ g; +- } +- s++; ++ xxh64_init(&ctx, 0); ++ while (*p != '\0') { ++ c = (unsigned char)i_toupper(*p++); ++ xxh64_loop(&ctx, &c, 1); + } +- +- return h; ++ return xxh64_to_32(xxh64_result(&ctx)); + } + + unsigned int ATTR_NO_SANITIZE_INTEGER +-- +2.47.3 + diff --git a/patches/0002-lib-compression-Iterate-instead-of-recursing-on-zero.patch b/patches/0002-lib-compression-Iterate-instead-of-recursing-on-zero.patch new file mode 100644 index 0000000..55c03c6 --- /dev/null +++ b/patches/0002-lib-compression-Iterate-instead-of-recursing-on-zero.patch @@ -0,0 +1,187 @@ +From 88ae095f0530417ccc619af0a9fb68b0ef7cf0c0 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 10 Jun 2026 11:54:00 +0000 +Subject: [PATCH 2/4] lib-compression: Iterate instead of recursing on + zero-output decompress chunks + +istream-lz4, istream-zlib and istream-bzlib each retried a read that +produced no output by tail-calling their own read function. A crafted +compressed stream can contain an unbounded number of chunks/steps that +each decompress to zero bytes - e.g. an lz4 stream of single-byte chunks +that each decode to nothing - so an attacker controlling the compressed +data can drive recursion depth proportional to the chunk count. Tail-call +optimization is not guaranteed, so this can exhaust the stack and crash +the process reading the stream. + +Replace the recursive calls with continue inside the for(;;) loop +introduced in the previous commit, keeping stack usage O(1). Add a +regression test that feeds istream-lz4 a stream of 100000 empty chunks +and reads it in a single call. +--- + src/lib-compression/istream-bzlib.c | 8 +++-- + src/lib-compression/istream-lz4.c | 16 ++++++--- + src/lib-compression/istream-zlib.c | 8 +++-- + src/lib-compression/test-compression.c | 50 ++++++++++++++++++++++++++ + 4 files changed, 74 insertions(+), 8 deletions(-) + +Index: trixie/src/lib-compression/istream-bzlib.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-bzlib.c ++++ trixie/src/lib-compression/istream-bzlib.c +@@ -51,6 +51,10 @@ static ssize_t i_stream_bzlib_read(struc + size_t size, out_size; + int ret; + ++ /* Loop instead of recursing when a decompress step yields no output: ++ a crafted stream can produce an unbounded number of zero-output ++ steps, and tail-call recursion (not guaranteed) would exhaust the ++ stack. */ + for (;;) { + high_offset = stream->istream.v_offset + (stream->pos - stream->skip); + if (zstream->eof_offset == high_offset) { +@@ -130,8 +134,8 @@ static ssize_t i_stream_bzlib_read(struc + i_fatal("BZ2_bzDecompress() failed with %d", ret); + } + if (out_size == 0) { +- /* read more input */ +- return i_stream_bzlib_read(stream); ++ /* no output yet; read more input without recursing */ ++ continue; + } + return out_size; + } +Index: trixie/src/lib-compression/istream-lz4.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-lz4.c ++++ trixie/src/lib-compression/istream-lz4.c +@@ -149,6 +149,10 @@ static ssize_t i_stream_lz4_read(struct + zstream->header_read = TRUE; + } + ++ /* Loop over chunks instead of recursing on zero-output chunks: a ++ crafted stream can contain an unbounded number of chunks that each ++ decompress to zero bytes, and tail-call recursion (which is not ++ guaranteed) would let that exhaust the stack. */ + for (;;) { + if (zstream->chunk_left == 0) { + while ((ret = i_stream_lz4_read_chunk_header(zstream)) == 0) { +@@ -183,8 +187,8 @@ static ssize_t i_stream_lz4_read(struct + if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream)) + return -2; + if (i_stream_get_data_size(zstream->istream.parent) > 0) { +- /* Parent stream was only partially consumed. Set the stream's +- IO as pending to avoid hangs. */ ++ /* Parent stream was only partially consumed. Set the ++ stream's IO as pending to avoid hangs. */ + i_stream_set_input_pending(&zstream->istream.istream, TRUE); + } + /* allocate enough space for the old data and the new +@@ -199,8 +203,12 @@ static ssize_t i_stream_lz4_read(struct + lz4_read_error(zstream, "corrupted lz4 chunk"); + stream->istream.stream_errno = EINVAL; + return -1; +- } else if (ret == 0) +- return i_stream_lz4_read(stream); ++ } else if (ret == 0) { ++ /* chunk decompressed to zero bytes; read the next chunk ++ without recursing so the stack stays bounded. */ ++ buffer_set_used_size(zstream->chunk_buf, 0); ++ continue; ++ } + i_assert(ret > 0); + stream->pos += ret; + i_assert(stream->pos <= stream->buffer_size); +Index: trixie/src/lib-compression/istream-zlib.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-zlib.c ++++ trixie/src/lib-compression/istream-zlib.c +@@ -168,6 +168,10 @@ static ssize_t i_stream_zlib_read(struct + size_t size, out_size; + int ret; + ++ /* Loop instead of recursing when an inflate step yields no output: ++ a crafted stream can produce an unbounded number of zero-output ++ steps, and tail-call recursion (not guaranteed) would exhaust the ++ stack. */ + for (;;) { + high_offset = stream->istream.v_offset + (stream->pos - stream->skip); + if (zstream->eof_offset == high_offset) { +@@ -314,8 +318,8 @@ static ssize_t i_stream_zlib_read(struct + i_fatal("inflate() failed with %d", ret); + } + if (out_size == 0) { +- /* read more input */ +- return i_stream_zlib_read(stream); ++ /* no output yet; read more input without recursing */ ++ continue; + } + return out_size; + } +Index: trixie/src/lib-compression/test-compression.c +=================================================================== +--- trixie.orig/src/lib-compression/test-compression.c ++++ trixie/src/lib-compression/test-compression.c +@@ -1108,6 +1108,55 @@ static void test_lz4_chunk_size(void) + test_end(); + } + ++static void test_lz4_many_empty_chunks(void) ++{ ++ const struct compression_handler *lz4; ++ struct istream *file_input, *input; ++ ++ if (compression_lookup_handler("lz4", &lz4) <= 0) ++ return; /* not compiled in or unknown */ ++ ++ test_begin("lz4 many empty chunks"); ++ ++ /* A crafted lz4 stream can contain an unbounded number of chunks that ++ each decompress to zero bytes. i_stream_lz4_read() must iterate over ++ them rather than recurse once per chunk, or the stack is exhausted ++ (tail-call optimization is not guaranteed). */ ++ buffer_t *buf = buffer_create_dynamic(default_pool, 1024*512); ++ struct iostream_lz4_header hdr; ++ memcpy(hdr.magic, IOSTREAM_LZ4_MAGIC, IOSTREAM_LZ4_MAGIC_LEN); ++ /* a valid (64k) max uncompressed chunk size, big-endian */ ++ hdr.max_uncompressed_chunk_size[0] = 0x00; ++ hdr.max_uncompressed_chunk_size[1] = 0x01; ++ hdr.max_uncompressed_chunk_size[2] = 0x00; ++ hdr.max_uncompressed_chunk_size[3] = 0x00; ++ buffer_append(buf, &hdr, sizeof(hdr)); ++ ++ /* Each chunk: 4-byte big-endian compressed length (1), then a single ++ 0x00 byte, which LZ4_decompress_safe() decodes to zero bytes. */ ++ static const unsigned char empty_chunk[] = { ++ 0x00, 0x00, 0x00, 0x01, 0x00 ++ }; ++ for (unsigned int i = 0; i < 100000; i++) ++ buffer_append(buf, empty_chunk, sizeof(empty_chunk)); ++ ++ file_input = test_istream_create_data(buf->data, buf->used); ++ file_input->blocking = TRUE; ++ input = lz4->create_istream(file_input); ++ i_stream_unref(&file_input); ++ ++ /* All chunks are empty: clean EOF, no content, no error - and, with ++ the iterative read, no stack overflow. */ ++ test_assert(i_stream_read(input) == -1); ++ test_assert(input->eof); ++ test_assert(input->stream_errno == 0); ++ ++ i_stream_unref(&input); ++ buffer_free(&buf); ++ ++ test_end(); ++} ++ + static void test_uncompress_file(const char *path) + { + const struct compression_handler *handler; +@@ -1232,6 +1281,7 @@ int main(int argc, char *argv[]) + test_gz_large_header, + test_lz4_small_header, + test_lz4_chunk_size, ++ test_lz4_many_empty_chunks, + test_compression_ext, + test_compression_deinit, + NULL diff --git a/patches/0002-lib-mail-o_stream_dot_sendv-Fix-boundary-off-by-one.patch b/patches/0002-lib-mail-o_stream_dot_sendv-Fix-boundary-off-by-one.patch new file mode 100644 index 0000000..a7ea806 --- /dev/null +++ b/patches/0002-lib-mail-o_stream_dot_sendv-Fix-boundary-off-by-one.patch @@ -0,0 +1,62 @@ +From a6d847f3ad7076a3406a84774a90ab58a67f7908 Mon Sep 17 00:00:00 2001 +From: Marco Bettini +Date: Mon, 20 Apr 2026 09:50:54 +0000 +Subject: [PATCH 2/4] lib-mail: o_stream_dot_sendv() - Fix boundary off by one + +--- + src/lib-mail/ostream-dot.c | 2 +- + src/lib-mail/test-ostream-dot.c | 23 +++++++++++++++++++++++ + 2 files changed, 24 insertions(+), 1 deletion(-) + +Index: trixie/src/lib-mail/ostream-dot.c +=================================================================== +--- trixie.orig/src/lib-mail/ostream-dot.c ++++ trixie/src/lib-mail/ostream-dot.c +@@ -105,7 +105,7 @@ o_stream_dot_sendv(struct ostream_privat + + p = data; + pend = CONST_PTR_OFFSET(data, size); +- for (; p < pend && (size_t)(p-data)+2 < max_bytes; p++) { ++ for (; p < pend && ((size_t)(p - data) + 2) <= max_bytes; p++) { + char add = 0; + + size = pend - p; +Index: trixie/src/lib-mail/test-ostream-dot.c +=================================================================== +--- trixie.orig/src/lib-mail/test-ostream-dot.c ++++ trixie/src/lib-mail/test-ostream-dot.c +@@ -92,11 +92,34 @@ static void test_ostream_dot_parent_almo + test_end(); + } + ++static void test_ostream_dot_parent_exact_fit(void) ++{ ++ buffer_t *output_data; ++ struct ostream *test_output, *output; ++ ssize_t ret; ++ ++ test_begin("dot ostream parent exact fit"); ++ output_data = t_buffer_create(1024); ++ test_output = test_ostream_create_nonblocking(output_data, 2); ++ test_ostream_set_max_output_size(test_output, 2); ++ ++ output = o_stream_create_dot(test_output, FALSE); ++ ret = o_stream_send(output, ".", 1); ++ test_assert(ret == 1); ++ test_assert_ucmp(output_data->used, ==, 2); ++ test_assert_memcmp(output_data->data, output_data->used, "..", 2); ++ ++ o_stream_unref(&output); ++ o_stream_unref(&test_output); ++ test_end(); ++} ++ + int main(void) + { + static void (*const test_functions[])(void) = { + test_ostream_dot, + test_ostream_dot_parent_almost_full, ++ test_ostream_dot_parent_exact_fit, + NULL + }; + return test_run(test_functions); diff --git a/patches/0002-lib-path-util-Add-t_openat_safe-for-jailed-openat-re.patch b/patches/0002-lib-path-util-Add-t_openat_safe-for-jailed-openat-re.patch new file mode 100644 index 0000000..32ef957 --- /dev/null +++ b/patches/0002-lib-path-util-Add-t_openat_safe-for-jailed-openat-re.patch @@ -0,0 +1,406 @@ +From 5cc55540eef2096680e6074c27ea0524a2ecc6d9 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 4 May 2026 13:08:16 +0000 +Subject: [PATCH 2/5] lib: path-util - Add t_openat_safe() for jailed openat() + resolution + +Add a helper that opens a path relative to a directory fd while refusing +any resolution that escapes that directory. Each component is opened with +O_NOFOLLOW so symlinks are detected explicitly rather than transparently +followed; symlinks are then resolved manually and only honoured when their +(recursively resolved) target also stays beneath the base fd. Absolute +symlink targets, leading '/', and '..' past the base are rejected with +errno=ELOOP. Symlink chains are bounded by +PATH_UTIL_OPENAT_SAFE_MAX_SYMLINKS hops. + +Anchoring resolution at a caller-held base_fd makes the lookup TOCTOU-safe +even when intermediate path components are mutated on disk concurrently: +the kernel resolves all openat() lookups relative to the original +directory inode that base_fd refers to, so an attacker mutating path +components on the filesystem cannot redirect resolution. + +This is the portable counterpart to Linux's openat2() with RESOLVE_BENEATH +and is intended for callers that need to safely look up files under a +user-writable directory (e.g. a Sieve script storage directory). +--- + src/lib/path-util.c | 170 +++++++++++++++++++++++++++++++++++++++ + src/lib/path-util.h | 29 +++++++ + src/lib/test-path-util.c | 124 ++++++++++++++++++++++++++++ + 3 files changed, 323 insertions(+) + +Index: trixie/src/lib/path-util.c +=================================================================== +--- trixie.orig/src/lib/path-util.c ++++ trixie/src/lib/path-util.c +@@ -4,12 +4,17 @@ + #include "str.h" + #include "path-util.h" + ++#include + #include + #include + #include + + #define PATH_UTIL_MAX_PATH 8*1024 + #define PATH_UTIL_MAX_SYMLINKS 80 ++/* Symlink-hop cap for t_openat_safe(). Lower than the realpath cap: ++ t_openat_safe() refuses absolute and escaping targets outright, so a ++ long chain inside the base dir is the only legitimate use. */ ++#define PATH_UTIL_OPENAT_SAFE_MAX_SYMLINKS 16 + + static int t_getcwd_noalloc(char **dir_r, size_t *asize_r, + const char **error_r) ATTR_NULL(2) +@@ -358,6 +363,171 @@ int t_readlink(const char *path, const c + return 0; + } + ++/* Iteratively resolve `path` beneath `cur_fd` using openat() with ++ O_NOFOLLOW per component. Symlinks are resolved manually: their targets ++ are validated (non-absolute, no '..' past the base) and the loop ++ restarts with the target spliced into the remaining path. dir_depth ++ tracks how many directory levels we have descended below the original ++ base; '..' is refused when the depth would go negative. ++ */ ++static int ++path_openat_safe_walk(int cur_fd, const char *path, int flags, ++ const char **error_r) ++{ ++ bool cur_owned = FALSE; ++ unsigned int symlink_count = 0; ++ unsigned int dir_depth = 0; ++ ++ /* Absolute paths cannot stay beneath the base directory. */ ++ if (*path == '/') { ++ errno = ELOOP; ++ *error_r = "Path is absolute (escapes base directory)"; ++ return -1; ++ } ++ ++ for (;;) { ++ while (*path == '/') ++ path++; ++ ++ if (*path == '\0') { ++ /* Empty path or trailing slash: open the current ++ directory itself with the requested flags. */ ++ int fd = openat(cur_fd, ".", ++ flags | O_NOFOLLOW | O_CLOEXEC); ++ if (cur_owned) ++ i_close_fd(&cur_fd); ++ if (fd < 0) { ++ *error_r = t_strdup_printf( ++ "openat() failed: %m"); ++ } ++ return fd; ++ } ++ ++ const char *slash = strchr(path, '/'); ++ size_t comp_len = (slash != NULL ? ++ (size_t)(slash - path) : strlen(path)); ++ bool is_last = (slash == NULL); ++ ++ if (comp_len == 1 && path[0] == '.') { ++ path += comp_len; ++ continue; ++ } ++ ++ if (comp_len == 2 && path[0] == '.' && path[1] == '.') { ++ if (dir_depth == 0) { ++ if (cur_owned) ++ i_close_fd(&cur_fd); ++ errno = ELOOP; ++ *error_r = "Path escapes base directory via '..'"; ++ return -1; ++ } ++ int parent_fd = openat(cur_fd, "..", ++ O_RDONLY | O_DIRECTORY | ++ O_NOFOLLOW | O_CLOEXEC); ++ if (cur_owned) ++ i_close_fd(&cur_fd); ++ if (parent_fd < 0) { ++ *error_r = t_strdup_printf( ++ "openat(..) failed: %m"); ++ return -1; ++ } ++ cur_fd = parent_fd; ++ cur_owned = TRUE; ++ dir_depth--; ++ path += comp_len; ++ continue; ++ } ++ ++ const char *comp = t_strdup_until(path, path + comp_len); ++ int next_fd; ++ if (is_last) { ++ next_fd = openat(cur_fd, comp, ++ flags | O_NOFOLLOW | O_CLOEXEC); ++ } else { ++ next_fd = openat(cur_fd, comp, ++ O_RDONLY | O_DIRECTORY | ++ O_NOFOLLOW | O_CLOEXEC); ++ } ++ ++ if (next_fd < 0 && (errno == ELOOP || errno == EMLINK)) { ++ /* Component is a symlink. Resolve it ourselves and ++ restart processing with its target spliced into ++ `path`; that way the target is itself walked ++ component-by-component and the boundary check ++ continues to apply. (O_NOFOLLOW on a symlink reports ++ ELOOP on Linux/macOS but EMLINK on the BSDs, so both ++ must be treated as "this is a symlink".) */ ++ if (++symlink_count > ++ PATH_UTIL_OPENAT_SAFE_MAX_SYMLINKS) { ++ if (cur_owned) ++ i_close_fd(&cur_fd); ++ errno = ELOOP; ++ *error_r = "Too many symlink dereferences"; ++ return -1; ++ } ++ ++ char buf[PATH_UTIL_MAX_PATH]; ++ ssize_t llen = readlinkat(cur_fd, comp, ++ buf, sizeof(buf) - 1); ++ if (llen < 0) { ++ if (cur_owned) ++ i_close_fd(&cur_fd); ++ *error_r = t_strdup_printf( ++ "readlinkat(%s) failed: %m", comp); ++ return -1; ++ } ++ buf[llen] = '\0'; ++ ++ if (buf[0] == '/') { ++ if (cur_owned) ++ i_close_fd(&cur_fd); ++ errno = ELOOP; ++ *error_r = t_strdup_printf( ++ "Symlink '%s' has absolute target " ++ "'%s' (escapes base directory)", ++ comp, buf); ++ return -1; ++ } ++ ++ path = is_last ? t_strdup(buf) : ++ t_strconcat(buf, "/", ++ slash + 1, NULL); ++ continue; ++ } ++ ++ if (next_fd < 0) { ++ if (cur_owned) ++ i_close_fd(&cur_fd); ++ *error_r = t_strdup_printf( ++ "openat(%s) failed: %m", comp); ++ return -1; ++ } ++ ++ if (is_last) { ++ if (cur_owned) ++ i_close_fd(&cur_fd); ++ return next_fd; ++ } ++ ++ if (cur_owned) ++ i_close_fd(&cur_fd); ++ cur_fd = next_fd; ++ cur_owned = TRUE; ++ dir_depth++; ++ path = slash + 1; ++ } ++} ++ ++int t_openat_safe(int base_fd, const char *path, int flags, ++ const char **error_r) ++{ ++ i_assert(base_fd >= 0); ++ i_assert(path != NULL); ++ i_assert(error_r != NULL); ++ ++ return path_openat_safe_walk(base_fd, path, flags, error_r); ++} ++ + bool t_binary_abspath(const char **binpath, const char **error_r) + { + const char *path_env, *const *paths; +Index: trixie/src/lib/path-util.h +=================================================================== +--- trixie.orig/src/lib/path-util.h ++++ trixie/src/lib/path-util.h +@@ -57,6 +57,35 @@ int t_get_working_dir(const char **dir_r + * -1 on failure. error_r is set on failure and cannot be NULL. */ + int t_readlink(const char *path, const char **dest_r, const char **error_r); + ++/* Open `path` relative to `base_fd`, refusing any resolution that escapes ++ the directory referenced by `base_fd`. Each path component is opened with ++ O_NOFOLLOW so symlinks are detected explicitly rather than transparently ++ followed; symlinks are followed only when their (recursively resolved) ++ target also stays beneath `base_fd`. Absolute symlink targets, leading ++ '/', and '..' past the base are rejected. ++ ++ `base_fd` must reference a directory (e.g. obtained via ++ open(dir, O_DIRECTORY|O_RDONLY|O_CLOEXEC)) and remains owned by the ++ caller; this function does not close it. ++ ++ `flags` is OR-ed into the openat() call for the final component (e.g. ++ O_RDONLY). O_NOFOLLOW and O_CLOEXEC are added implicitly. ++ ++ Anchoring resolution at `base_fd` makes the lookup TOCTOU-safe even when ++ intermediate path components are mutated on disk concurrently: the kernel ++ resolves all component lookups relative to the original directory inode ++ `base_fd` references. ++ ++ Returns the new fd (>= 0) on success. On failure returns -1 with errno ++ set and *error_r set to a descriptive message: ++ errno=ELOOP target escapes base, or symlink chain too deep ++ errno=ENOENT file not found ++ errno=EACCES permission denied ++ other errno as set by openat()/readlinkat() ++ */ ++int t_openat_safe(int base_fd, const char *path, int flags, ++ const char **error_r); ++ + /* Update binpath to be absolute: + * a) begins with '/' -> no change + * b) contains '/' -> assume relative to working directory +Index: trixie/src/lib/test-path-util.c +=================================================================== +--- trixie.orig/src/lib/test-path-util.c ++++ trixie/src/lib/test-path-util.c +@@ -233,6 +233,129 @@ static void test_link_alloc2(void) + tmpdir = o_tmpdir; + } + ++static void test_openat_safe(void) ++{ ++ const char *error; ++ int base_fd, fd; ++ ++ base_fd = open(tmpdir, O_RDONLY | O_DIRECTORY | O_CLOEXEC); ++ if (base_fd < 0) ++ i_fatal("open(%s) failed: %m", tmpdir); ++ ++ /* Plain regular file inside base. */ ++ const char *plain = t_strconcat(tmpdir, "/plain", NULL); ++ int wfd = creat(plain, 0600); ++ if (wfd < 0) ++ i_fatal("creat(%s) failed: %m", plain); ++ i_close_fd(&wfd); ++ ++ fd = t_openat_safe(base_fd, "plain", O_RDONLY, &error); ++ test_assert(fd >= 0); ++ i_close_fd(&fd); ++ ++ /* "./plain" and redundant slashes. */ ++ fd = t_openat_safe(base_fd, "./plain", O_RDONLY, &error); ++ test_assert(fd >= 0); ++ i_close_fd(&fd); ++ fd = t_openat_safe(base_fd, ".//plain", O_RDONLY, &error); ++ test_assert(fd >= 0); ++ i_close_fd(&fd); ++ ++ /* Symlink whose target is inside the base: allowed. */ ++ const char *in_link = t_strconcat(tmpdir, "/in-link", NULL); ++ if (symlink("plain", in_link) < 0) ++ i_fatal("symlink(plain, %s) failed: %m", in_link); ++ fd = t_openat_safe(base_fd, "in-link", O_RDONLY, &error); ++ test_assert(fd >= 0); ++ i_close_fd(&fd); ++ ++ /* Absolute symlink target: refused. */ ++ const char *abs_link = t_strconcat(tmpdir, "/abs-link", NULL); ++ if (symlink("/etc/hostname", abs_link) < 0) ++ i_fatal("symlink failed: %m"); ++ errno = 0; ++ fd = t_openat_safe(base_fd, "abs-link", O_RDONLY, &error); ++ test_assert(fd == -1); ++ test_assert(errno == ELOOP); ++ test_assert(error != NULL); ++ ++ /* Relative symlink with '..' that escapes the base: refused. */ ++ const char *escape_link = t_strconcat(tmpdir, "/escape-link", NULL); ++ if (symlink("../../etc/hostname", escape_link) < 0) ++ i_fatal("symlink failed: %m"); ++ errno = 0; ++ fd = t_openat_safe(base_fd, "escape-link", O_RDONLY, &error); ++ test_assert(fd == -1); ++ test_assert(errno == ELOOP); ++ test_assert(error != NULL); ++ ++ /* Direct '..' escape attempt: refused. */ ++ errno = 0; ++ fd = t_openat_safe(base_fd, "../plain", O_RDONLY, &error); ++ test_assert(fd == -1); ++ test_assert(errno == ELOOP); ++ ++ /* Absolute literal path: refused. */ ++ errno = 0; ++ fd = t_openat_safe(base_fd, "/etc/hostname", O_RDONLY, &error); ++ test_assert(fd == -1); ++ test_assert(errno == ELOOP); ++ ++ /* link2's target is an absolute path (tmpdir/link3): refused as an ++ absolute target before the loop is ever followed. */ ++ errno = 0; ++ fd = t_openat_safe(base_fd, "link2", O_RDONLY, &error); ++ test_assert(fd == -1); ++ test_assert(errno == ELOOP); ++ ++ /* Relative symlink loop staying inside the base: nothing rejects it ++ except the hop counter, so this exercises ++ PATH_UTIL_OPENAT_SAFE_MAX_SYMLINKS. */ ++ const char *loopa = t_strconcat(tmpdir, "/loopa", NULL); ++ const char *loopb = t_strconcat(tmpdir, "/loopb", NULL); ++ if (symlink("loopb", loopa) < 0) ++ i_fatal("symlink(loopb, %s) failed: %m", loopa); ++ if (symlink("loopa", loopb) < 0) ++ i_fatal("symlink(loopa, %s) failed: %m", loopb); ++ errno = 0; ++ fd = t_openat_safe(base_fd, "loopa", O_RDONLY, &error); ++ test_assert(fd == -1); ++ test_assert(errno == ELOOP); ++ test_assert(error != NULL); ++ ++ /* '..' that stays within the base: allowed. Create sub/under and ++ reach back to plain via sub/../plain. */ ++ const char *subdir = t_strconcat(tmpdir, "/sub", NULL); ++ if (mkdir(subdir, 0700) < 0 && errno != EEXIST) ++ i_fatal("mkdir(%s) failed: %m", subdir); ++ fd = t_openat_safe(base_fd, "sub/../plain", O_RDONLY, &error); ++ test_assert(fd >= 0); ++ i_close_fd(&fd); ++ ++ /* Breaking out of the base via subdirectory. */ ++ errno = 0; ++ fd = t_openat_safe(base_fd, "sub/../../plain", O_RDONLY, &error); ++ test_assert(fd == -1); ++ test_assert(errno == ELOOP); ++ ++ /* Nonexistent file inside base: ENOENT, not ELOOP. */ ++ errno = 0; ++ fd = t_openat_safe(base_fd, "no-such-file", O_RDONLY, &error); ++ test_assert(fd == -1); ++ test_assert(errno == ENOENT); ++ ++ i_close_fd(&base_fd); ++ ++ i_unlink(plain); ++ i_unlink(in_link); ++ i_unlink(abs_link); ++ i_unlink(escape_link); ++ i_unlink(loopa); ++ i_unlink(loopb); ++ if (rmdir(subdir) < 0) ++ i_error("rmdir(%s) failed: %m", subdir); ++} ++ + static void test_cleanup(void) + { + const char *error; +@@ -272,6 +395,7 @@ void test_path_util(void) + test_abspath_vs_normpath(); + test_link_alloc(); + test_link_alloc2(); ++ test_openat_safe(); + test_cleanup(); + alarm(0); + test_end(); diff --git a/patches/0002-lib-sieve-Drop-binary-header-resource-usage-tracking.patch b/patches/0002-lib-sieve-Drop-binary-header-resource-usage-tracking.patch new file mode 100644 index 0000000..35fe8b2 --- /dev/null +++ b/patches/0002-lib-sieve-Drop-binary-header-resource-usage-tracking.patch @@ -0,0 +1,304 @@ +From 64e1f7af273809f484fb983d4a6edb1af1a85c99 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 5 May 2026 16:19:20 +0000 +Subject: [PATCH 2/7] lib-sieve: Drop binary-header resource usage tracking + +With the per-user sieve-rusage file now authoritative for personal +storages, the .svbin header no longer needs to carry CPU usage. Admin +storages (sieve_before/after, default location) cannot be re-uploaded +by the user, so cross-run tracking is unnecessary there; the runtime +sieve_max_cpu_time check still enforces the per-execution limit. + +Move the in-memory cumulative carry-over from sbin->header.resource_usage +to a dedicated sbin->persisted_rusage substruct. The on-disk header +struct now solely represents the wire format and is zeroed on every +save; the in-memory carry-over no longer overloads it. Drop the now-dead +"resource usage" section from sieve_binary_dumper_run() (the on-disk +fields are always zero, so the section never produced output). +--- + src/lib-sieve/sieve-binary-dumper.c | 12 --- + src/lib-sieve/sieve-binary-file.c | 141 +++++---------------------- + src/lib-sieve/sieve-binary-private.h | 6 ++ + src/lib-sieve/sieve-binary.c | 15 ++- + 4 files changed, 37 insertions(+), 137 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-dumper.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-dumper.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-dumper.c +@@ -6,7 +6,6 @@ + #include "ostream.h" + #include "array.h" + #include "buffer.h" +-#include "time-util.h" + + #include "sieve-common.h" + #include "sieve-extensions.h" +@@ -117,17 +116,6 @@ bool sieve_binary_dumper_run(struct siev + "flags = 0x%08"PRIx32"\n", + header->version_major, header->version_minor, + header->flags); +- if (header->resource_usage.update_time != 0) { +- time_t update_time = +- (time_t)header->resource_usage.update_time; +- sieve_binary_dumpf(denv, +- "resource usage:\n" +- " update time = %s\n" +- " cpu time = %"PRIu32" ms\n", +- t_strflocaltime("%Y-%m-%d %H:%M:%S", +- update_time), +- header->resource_usage.cpu_time_msecs); +- } + + /* Dump list of binary blocks */ + +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-file.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c +@@ -147,47 +147,15 @@ sieve_binary_file_read_header(struct sie + return 0; + } + +-static int +-sieve_binary_file_write_header(struct sieve_binary *sbin, int fd, +- struct sieve_binary_header *header, +- enum sieve_error *error_code_r) +-{ +- ssize_t wret; +- +- wret = pwrite(fd, header, sizeof(*header), 0); +- if (wret < 0) { +- e_error(sbin->event, "update: " +- "failed to write to binary: %m"); +- *error_code_r = SIEVE_ERROR_TEMP_FAILURE; +- return -1; +- } else if (wret != sizeof(*header)) { +- e_error(sbin->event, "update: " +- "header written partially %zd/%zu", +- wret, sizeof(*header)); +- *error_code_r = SIEVE_ERROR_TEMP_FAILURE; +- return -1; +- } +- return 0; +-} +- +-static void sieve_binary_file_update_header(struct sieve_binary *sbin) ++/* The on-disk binary header carries an unused resource_usage struct for ++ backward compatibility. Tracking lives in the per-user sieve-rusage file. */ ++static void sieve_binary_file_zero_header_rusage(struct sieve_binary *sbin) + { + struct sieve_binary_header *header = &sbin->header; +- struct sieve_resource_usage rusage; +- +- sieve_binary_get_resource_usage(sbin, &rusage); + + i_zero(&header->resource_usage); +- if (HAS_ALL_BITS(header->flags, SIEVE_BINARY_FLAG_RESOURCE_LIMIT) || +- sieve_resource_usage_is_high(sbin->svinst, &rusage)) { +- header->resource_usage.update_time = ioloop_time; +- header->resource_usage.cpu_time_msecs = rusage.cpu_time_msecs; +- } +- + sieve_resource_usage_init(&sbin->rusage); + sbin->rusage_updated = FALSE; +- +- (void)sieve_binary_check_resource_usage(sbin); + } + + /* +@@ -347,7 +315,7 @@ sieve_binary_save_to_stream(struct sieve + header->hdr_size = sizeof(*header); + + header->flags &= ENUM_NEGATE(SIEVE_BINARY_FLAG_RESOURCE_LIMIT); +- sieve_binary_file_update_header(sbin); ++ sieve_binary_file_zero_header_rusage(sbin); + + if (!_save_aligned(sbin, stream, header, sizeof(*header), NULL)) { + e_error(sbin->event, "save: failed to save header"); +@@ -864,6 +832,11 @@ _sieve_binary_open(struct sieve_binary * + return FALSE; + offset = sbin->header.hdr_size; + ++ /* Resource usage tracking is no longer kept in the binary file; ++ discard any data from older versions. */ ++ i_zero(&sbin->header.resource_usage); ++ sbin->header.flags &= ENUM_NEGATE(SIEVE_BINARY_FLAG_RESOURCE_LIMIT); ++ + /* Load block index */ + + for (i = 0; i < sbin->header.blocks && result; i++) { +@@ -979,47 +952,27 @@ int sieve_binary_check_executable(struct + * Resource usage + */ + +-static int +-sieve_binary_file_do_update_resource_usage( +- struct sieve_binary *sbin, int fd, enum sieve_error *error_code_r) +-{ +- struct sieve_binary_header *header = &sbin->header; +- struct file_lock *lock; +- const char *error; +- int ret; +- +- struct file_lock_settings lock_set = { +- .lock_method = FILE_LOCK_METHOD_FCNTL, +- }; +- ret = file_wait_lock(fd, sbin->path, F_WRLCK, &lock_set, +- SIEVE_BINARY_FILE_LOCK_TIMEOUT, &lock, &error); +- if (ret <= 0) { +- e_error(sbin->event, "%s", error); +- *error_code_r = SIEVE_ERROR_TEMP_FAILURE; +- return -1; +- } +- +- ret = sieve_binary_file_read_header(sbin, fd, header, error_code_r); +- if (ret == 0) { +- sieve_binary_file_update_header(sbin); +- ret = sieve_binary_file_write_header(sbin, fd, header, +- error_code_r); +- } +- +- file_lock_free(&lock); +- +- return ret; +-} +- +-static int +-sieve_binary_storage_update_resource_usage(struct sieve_binary *sbin, +- struct sieve_storage *storage, +- enum sieve_error *error_code_r) ++int sieve_binary_file_update_resource_usage(struct sieve_binary *sbin, ++ enum sieve_error *error_code_r) + { ++ struct sieve_storage *storage; + struct sieve_resource_usage delta; + struct sieve_resource_usage total; + uint32_t flags; + ++ sieve_error_args_init(&error_code_r, NULL); ++ ++ sieve_binary_file_close(&sbin->file); ++ ++ if (sbin->script == NULL || sbin->script->storage == NULL || ++ sbin->script->storage->rusage_path == NULL) { ++ /* No per-user file: tracking discarded. */ ++ sieve_resource_usage_init(&sbin->rusage); ++ sbin->rusage_updated = FALSE; ++ return 0; ++ } ++ storage = sbin->script->storage; ++ + delta = sbin->rusage; + sieve_binary_get_resource_usage(sbin, &total); + flags = sbin->header.flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT; +@@ -1041,47 +994,3 @@ sieve_binary_storage_update_resource_usa + sbin->rusage_updated = FALSE; + return 0; + } +- +-int sieve_binary_file_update_resource_usage(struct sieve_binary *sbin, +- enum sieve_error *error_code_r) +-{ +- struct sieve_storage *storage = NULL; +- int fd, ret = 0; +- +- sieve_error_args_init(&error_code_r, NULL); +- +- sieve_binary_file_close(&sbin->file); +- +- if (sbin->script != NULL && sbin->script->storage != NULL && +- sbin->script->storage->rusage_path != NULL) +- storage = sbin->script->storage; +- +- if (storage != NULL) { +- return sieve_binary_storage_update_resource_usage( +- sbin, storage, error_code_r); +- } +- +- if (sbin->path == NULL) +- return 0; +- if (sbin->header.version_major != SIEVE_BINARY_VERSION_MAJOR) { +- return sieve_binary_save(sbin, sbin->path, TRUE, 0600, +- error_code_r); +- } +- +- fd = sieve_binary_fd_open(sbin, sbin->path, O_RDWR, error_code_r); +- if (fd < 0) { +- i_assert(*error_code_r != SIEVE_ERROR_NONE); +- return -1; +- } +- +- ret = sieve_binary_file_do_update_resource_usage(sbin, fd, +- error_code_r); +- i_assert(ret == 0 || *error_code_r != SIEVE_ERROR_NONE); +- +- if (close(fd) < 0) { +- e_error(sbin->event, "update: " +- "failed to close: close() failed: %m"); +- } +- +- return ret; +-} +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-private.h ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h +@@ -98,6 +98,12 @@ struct sieve_binary { + struct sieve_binary_header header; + struct sieve_resource_usage rusage; + ++ /* Cumulative CPU usage loaded from per-user sieve-rusage file */ ++ struct { ++ time_t update_time; ++ uint32_t cpu_time_msecs; ++ } persisted_rusage; ++ + /* When the binary is loaded into memory or when it is being constructed + by the generator, extensions can be associated to the binary. The + extensions array is a sequential list of all linked extensions. The +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary.c +@@ -218,8 +218,8 @@ void sieve_binary_apply_persisted_rusage + if (sieve_rusage_storage_load(storage, &rusage, &flags) <= 0) + return; + +- header->resource_usage.cpu_time_msecs = rusage.cpu_time_msecs; +- header->resource_usage.update_time = ioloop_time; ++ sbin->persisted_rusage.cpu_time_msecs = rusage.cpu_time_msecs; ++ sbin->persisted_rusage.update_time = ioloop_time; + header->flags |= (flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT); + sieve_resource_usage_init(&sbin->rusage); + sbin->rusage_updated = FALSE; +@@ -228,15 +228,14 @@ void sieve_binary_apply_persisted_rusage + void sieve_binary_get_resource_usage(struct sieve_binary *sbin, + struct sieve_resource_usage *rusage_r) + { +- struct sieve_binary_header *header = &sbin->header; +- time_t update_time = header->resource_usage.update_time; ++ time_t update_time = sbin->persisted_rusage.update_time; + unsigned int timeout = sbin->svinst->set->resource_usage_timeout; + + if (update_time != 0 && (ioloop_time - update_time) > (time_t)timeout) +- i_zero(&header->resource_usage); ++ i_zero(&sbin->persisted_rusage); + + sieve_resource_usage_init(rusage_r); +- rusage_r->cpu_time_msecs = header->resource_usage.cpu_time_msecs; ++ rusage_r->cpu_time_msecs = sbin->persisted_rusage.cpu_time_msecs; + sieve_resource_usage_add(rusage_r, &sbin->rusage); + } + +@@ -278,9 +277,7 @@ bool sieve_binary_record_resource_usage( + void sieve_binary_set_resource_usage(struct sieve_binary *sbin, + const struct sieve_resource_usage *rusage) + { +- struct sieve_binary_header *header = &sbin->header; +- +- i_zero(&header->resource_usage); ++ i_zero(&sbin->persisted_rusage); + sbin->rusage = *rusage; + sbin->rusage_updated = TRUE; + diff --git a/patches/0002-lib-sieve-actions-Create-fileinto-mailbox-without-th.patch b/patches/0002-lib-sieve-actions-Create-fileinto-mailbox-without-th.patch new file mode 100644 index 0000000..9b4e91f --- /dev/null +++ b/patches/0002-lib-sieve-actions-Create-fileinto-mailbox-without-th.patch @@ -0,0 +1,146 @@ +From eec184745fabe1b2da0d5cc343f2a46710b70fc7 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 11 Jun 2026 19:33:23 +0000 +Subject: [PATCH 2/2] lib-sieve: actions - Create fileinto mailbox without the + autocreate flag + +Stop setting MAILBOX_FLAG_AUTO_CREATE/SUBSCRIBE in act_store. Instead, on +mailbox_open() returning MAIL_ERROR_NOTFOUND, create the mailbox explicitly +with sieve_act_store_create_mailbox() when autocreation is enabled (the +:create side effect already creates it explicitly). + +This makes the creation go through the normal ACL CREATE gate, so a fileinto +(with lda_mailbox_autocreate=yes) or fileinto :create can no longer create a +mailbox in a shared or public namespace that the user lacks the CREATE right +for. + +A permission failure to create the mailbox is classified by +sieve_act_store_create_error_status(): for a user's personal script it is a +permanent failure (implicit keep), while for a global (administrator-defined) +script it is deferred (temporary failure), so the administrator notices the +script is behaving in a way they did not intend. +--- + src/lib-sieve/sieve-actions.c | 56 +++++++++++++++++++++++++++-------- + src/lib-sieve/sieve-actions.h | 11 ++++++- + 2 files changed, 53 insertions(+), 14 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/sieve-actions.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-actions.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-actions.c +@@ -372,6 +372,31 @@ void sieve_act_store_get_storage_error(c + &trans->error_code)); + } + ++int sieve_act_store_create_error_status( ++ const struct sieve_action_exec_env *aenv, enum mail_error error_code) ++{ ++ const struct sieve_execute_env *eenv = aenv->exec_env; ++ ++ switch (error_code) { ++ case MAIL_ERROR_NONE: ++ return SIEVE_EXEC_OK; ++ case MAIL_ERROR_TEMP: ++ return SIEVE_EXEC_TEMP_FAILURE; ++ case MAIL_ERROR_PERM: ++ /* The mailbox cannot be created because of insufficient ++ permissions. This is a permanent condition. For a user's ++ personal script, fall through to implicit keep rather than ++ deferring the message. For a global (administrator-defined) ++ script, defer instead, so that the administrator notices the ++ script is behaving in a way they did not intend. */ ++ if ((eenv->flags & SIEVE_EXECUTE_FLAG_NOGLOBAL) != 0) ++ return SIEVE_EXEC_FAILURE; ++ return SIEVE_EXEC_TEMP_FAILURE; ++ default: ++ return SIEVE_EXEC_FAILURE; ++ } ++} ++ + int sieve_act_store_create_mailbox(const struct sieve_action_exec_env *aenv, + struct act_store_transaction *trans) + { +@@ -384,13 +409,13 @@ int sieve_act_store_create_mailbox(const + /* Create mailbox */ + if (mailbox_create(box, NULL, FALSE) < 0) { + sieve_act_store_get_storage_error(aenv, trans); +- if (trans->error_code == MAIL_ERROR_EXISTS) { +- trans->error = NULL; +- trans->error_code = MAIL_ERROR_NONE; +- } else { +- return (trans->error_code == MAIL_ERROR_TEMP ? +- SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE); ++ if (trans->error_code != MAIL_ERROR_EXISTS) { ++ return sieve_act_store_create_error_status( ++ aenv, trans->error_code); + } ++ /* Lost a race with another session; the mailbox exists now. */ ++ trans->error = NULL; ++ trans->error_code = MAIL_ERROR_NONE; + } + + /* Subscribe to it if necessary */ +@@ -402,10 +427,9 @@ int sieve_act_store_create_mailbox(const + + /* Try opening again */ + if (mailbox_open(box) < 0) { +- /* Failed definitively */ + sieve_act_store_get_storage_error(aenv, trans); +- return (trans->error_code == MAIL_ERROR_TEMP ? +- SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE); ++ return sieve_act_store_create_error_status( ++ aenv, trans->error_code); + } + return SIEVE_EXEC_OK; + } +@@ -434,10 +458,6 @@ act_store_mailbox_alloc(const struct sie + return FALSE; + } + +- if (eenv->scriptenv->mailbox_autocreate) +- flags |= MAILBOX_FLAG_AUTO_CREATE; +- if (eenv->scriptenv->mailbox_autosubscribe) +- flags |= MAILBOX_FLAG_AUTO_SUBSCRIBE; + *box_r = box = mailbox_alloc_for_user(eenv->scriptenv->user, mailbox, + flags); + *storage = mailbox_get_storage(box); +@@ -640,6 +660,16 @@ act_store_execute(const struct sieve_act + } + } + ++ /* Create the mailbox if it does not exist yet and autocreation is ++ enabled (e.g. lda_mailbox_autocreate=yes). The :create side effect ++ creates it earlier; this handles a plain fileinto. */ ++ if (trans->error_code == MAIL_ERROR_NOTFOUND && ++ eenv->scriptenv->mailbox_autocreate) { ++ int cstatus = sieve_act_store_create_mailbox(aenv, trans); ++ if (cstatus != SIEVE_EXEC_OK) ++ return cstatus; ++ } ++ + /* Exit early if transaction already failed */ + switch (trans->error_code) { + case MAIL_ERROR_NONE: +Index: trixie/pigeonhole/src/lib-sieve/sieve-actions.h +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-actions.h ++++ trixie/pigeonhole/src/lib-sieve/sieve-actions.h +@@ -246,8 +246,17 @@ void sieve_act_store_add_flags(const str + void sieve_act_store_get_storage_error(const struct sieve_action_exec_env *aenv, + struct act_store_transaction *trans); + ++/* Map a failure to create the target mailbox to an execution status. A ++ permanent failure (e.g. the user lacks permission to create the mailbox) ++ makes a user's personal script fall through to implicit keep, but is ++ deferred for a global (administrator-defined) script, so that the ++ unintended behaviour is surfaced to the administrator. */ ++int sieve_act_store_create_error_status( ++ const struct sieve_action_exec_env *aenv, enum mail_error error_code); ++ + /* Create the target mailbox of the store transaction (and subscribe to it +- when configured). */ ++ when configured). Returns SIEVE_EXEC_OK on success, or the status from ++ sieve_act_store_create_error_status() on failure. */ + int sieve_act_store_create_mailbox(const struct sieve_action_exec_env *aenv, + struct act_store_transaction *trans); + diff --git a/patches/0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch b/patches/0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch new file mode 100644 index 0000000..e459edd --- /dev/null +++ b/patches/0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch @@ -0,0 +1,129 @@ +From 915cb2edc90da04d8a7a122fef7932cd55e3fff2 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 4 May 2026 13:10:13 +0000 +Subject: [PATCH 2/2] lib-sieve: storage: file - Validate script stat path + through dir_fd + +Extend the symlink-escape protection added in the previous commit to the +stat performed by sieve_file_script_open(): an "include :personal" lookup +or any other indirect path that triggers sieve_file_script_stat() also +needs to refuse a symlink whose target leaves the personal storage +directory, otherwise the existence check succeeds and the file is opened +later via the safe path with an unhelpful "permission denied". + +Add sieve_file_script_stat_safe(), which uses fstatat(AT_SYMLINK_NOFOLLOW) +to obtain the entry's own stat (lnk_st) and then, only if the entry is a +symlink, opens it through sieve_file_storage_open_safe() to validate the +target stays inside dir_fd and to fetch the resolved target's stat (st) +via fstat(). Non-symlink entries skip the open entirely. + +Use the new helper in sieve_file_script_open() whenever fstorage->dir_fd +is available, falling back to the unsafe lstat+stat variant for +non-personal or single-file storages. +--- + .../storage/file/sieve-file-script.c | 78 ++++++++++++++++++- + 1 file changed, 75 insertions(+), 3 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c ++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c +@@ -285,6 +285,57 @@ sieve_file_script_stat(const char *path, + return 0; + } + ++/* TOCTOU-safe variant of sieve_file_script_stat() for directory storages. ++ Resolves the script entry beneath fstorage->dir_fd, refusing symlinks ++ whose target escapes the storage directory. *st gets the (resolved) ++ target's stat; *lnk_st gets the directory entry's own stat (i.e. ++ AT_SYMLINK_NOFOLLOW), matching the semantics of the unsafe variant. */ ++static int ++sieve_file_script_stat_safe(struct sieve_file_storage *fstorage, ++ const char *filename, struct stat *st, ++ struct stat *lnk_st, const char **error_r) ++{ ++ int fd, saved_errno; ++ ++ i_assert(fstorage->dir_fd >= 0); ++ ++ if (fstatat(fstorage->dir_fd, filename, lnk_st, ++ AT_SYMLINK_NOFOLLOW) < 0) { ++ *error_r = t_strdup_printf( ++ "fstatat(%s/%s) failed: %m", ++ fstorage->path, filename); ++ return -1; ++ } ++ ++ if (!S_ISLNK(lnk_st->st_mode)) { ++ *st = *lnk_st; ++ return 0; ++ } ++ ++ /* Symlink: open it via the safe walker (which refuses any target ++ that leaves fstorage->dir_fd) and read st from the resulting fd. ++ The fd is only used to fetch the resolved stat and is closed ++ immediately. Open with O_NONBLOCK so that a target which resolves ++ to a FIFO or other special file inside the storage directory only ++ yields its stat instead of blocking the delivery process. */ ++ if (sieve_file_storage_open_safe(fstorage, filename, ++ O_RDONLY | O_NONBLOCK, ++ &fd, error_r) < 0) ++ return -1; ++ ++ if (fstat(fd, st) < 0) { ++ saved_errno = errno; ++ *error_r = t_strdup_printf( ++ "fstat() failed for '%s/%s': %m", ++ fstorage->path, filename); ++ i_close_fd(&fd); ++ errno = saved_errno; ++ return -1; ++ } ++ i_close_fd(&fd); ++ return 0; ++} ++ + static const char * + path_split_filename(const char *path, const char **dir_path_r) + { +@@ -353,7 +404,25 @@ static int sieve_file_script_open(struct + dir_path = path; + + path = sieve_file_storage_path_extend(fstorage, filename); +- ret = sieve_file_script_stat(path, &st, &lnk_st); ++ if (fstorage->dir_fd >= 0) { ++ const char *serror; ++ ++ ret = sieve_file_script_stat_safe( ++ fstorage, filename, &st, ++ &lnk_st, &serror); ++ if (ret < 0 && errno == ELOOP) { ++ sieve_script_set_critical( ++ script, ++ "Failed to open sieve script: %s", ++ serror); ++ script->storage->error_code = ++ SIEVE_ERROR_NO_PERMISSION; ++ success = FALSE; ++ } ++ } else { ++ ret = sieve_file_script_stat( ++ path, &st, &lnk_st); ++ } + } + + } else { +@@ -451,8 +520,11 @@ sieve_file_script_get_stream(struct siev + + /* For directory-based storage, open the script via the storage + directory fd so that path resolution refuses to follow symlinks +- whose (recursive) target leaves the storage directory. +- Single-file storages have no dir_fd, so fall back to plain open(). */ ++ whose (recursive) target leaves the storage directory. The fd-based ++ walk also makes this TOCTOU-safe: even if intermediate path ++ components are mutated between the stat in sieve_file_script_open() ++ and this call, the resolution stays within the original directory. ++ Single-file storages have no dir_fd, so fall back to a plain open(). */ + if (fstorage->dir_fd >= 0 && fscript->filename != NULL && + *fscript->filename != '\0') { + if (sieve_file_storage_open_safe(fstorage, fscript->filename, diff --git a/patches/0002-lib-sieve-util-test-edit-mail-Adjust-to-changes-in-d.patch b/patches/0002-lib-sieve-util-test-edit-mail-Adjust-to-changes-in-d.patch new file mode 100644 index 0000000..6fc833e --- /dev/null +++ b/patches/0002-lib-sieve-util-test-edit-mail-Adjust-to-changes-in-d.patch @@ -0,0 +1,104 @@ +From fe17ee9e58d2eb7dab38976518380279112c0bca Mon Sep 17 00:00:00 2001 +From: Stephan Bosch +Date: Wed, 29 Oct 2025 17:49:10 +0100 +Subject: [PATCH 2/3] lib-sieve: util: test-edit-mail - Adjust to changes in + dovecot core lib-test + +--- + src/lib-sieve/util/test-edit-mail.c | 20 ++++++++++---------- + 1 file changed, 10 insertions(+), 10 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/util/test-edit-mail.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/util/test-edit-mail.c ++++ trixie/pigeonhole/src/lib-sieve/util/test-edit-mail.c +@@ -99,7 +99,7 @@ static void test_deinit_mail_user() + i_error("unlink_directory(%s) failed: %s", mail_home, error); + } + +-static void test_init(void) ++static void test_edit_mail_init(void) + { + test_pool = pool_alloconly_create(MEMPOOL_GROWING"test pool", 128); + +@@ -107,7 +107,7 @@ static void test_init(void) + test_raw_mail_user = mail_raw_user_create(test_mail_user); + } + +-static void test_deinit(void) ++static void test_edit_mail_deinit(void) + { + mail_user_unref(&test_raw_mail_user); + test_deinit_mail_user(); +@@ -186,7 +186,7 @@ static void test_edit_mail_concatenated( + const char *value; + + test_begin("edit-mail - concatenated"); +- test_init(); ++ test_edit_mail_init(); + + /* Compose the message */ + +@@ -434,7 +434,7 @@ static void test_edit_mail_concatenated( + edit_mail_unwrap(&edmail); + mail_raw_close(&rawmail); + i_stream_unref(&input_msg); +- test_deinit(); ++ test_edit_mail_deinit(); + test_end(); + } + +@@ -696,7 +696,7 @@ static void test_edit_mail_big_header(vo + const char *value; + + test_begin("edit-mail - big header"); +- test_init(); ++ test_edit_mail_init(); + + /* compose the message */ + +@@ -737,7 +737,7 @@ static void test_edit_mail_big_header(vo + edit_mail_unwrap(&edmail); + mail_raw_close(&rawmail); + i_stream_unref(&input_msg); +- test_deinit(); ++ test_edit_mail_deinit(); + test_end(); + } + +@@ -757,7 +757,7 @@ static void test_edit_mail_small_buffer( + unsigned int i; + + test_begin("edit-mail - small buffer"); +- test_init(); ++ test_edit_mail_init(); + + /* compose the message */ + +@@ -806,7 +806,7 @@ static void test_edit_mail_small_buffer( + edit_mail_unwrap(&edmail); + mail_raw_close(&rawmail); + i_stream_unref(&input_msg); +- test_deinit(); ++ test_edit_mail_deinit(); + test_end(); + } + +@@ -820,7 +820,7 @@ static void test_edit_mail_empty(void) + const char *value; + + test_begin("edit-mail - empty message"); +- test_init(); ++ test_edit_mail_init(); + + /* Compose the message */ + +@@ -862,7 +862,7 @@ static void test_edit_mail_empty(void) + edit_mail_unwrap(&edmail); + mail_raw_close(&rawmail); + i_stream_unref(&input_msg); +- test_deinit(); ++ test_edit_mail_deinit(); + test_end(); + } + diff --git a/patches/0002-lib-sql-Extract-sql_query_callback_delayed-helper.patch b/patches/0002-lib-sql-Extract-sql_query_callback_delayed-helper.patch new file mode 100644 index 0000000..92e43fe --- /dev/null +++ b/patches/0002-lib-sql-Extract-sql_query_callback_delayed-helper.patch @@ -0,0 +1,53 @@ +From 9582563d2c261cda1cf7c59fb262292d1438cd73 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Fri, 29 May 2026 11:21:26 +0000 +Subject: [PATCH 2/6] lib-sql: Extract sql_query_callback_delayed() helper + +--- + src/lib-sql/sql-api.c | 23 ++++++++++++++++------- + 1 file changed, 16 insertions(+), 7 deletions(-) + +diff --git a/src/lib-sql/sql-api.c b/src/lib-sql/sql-api.c +index 4db6913229..5ee13f9b32 100644 +--- a/src/lib-sql/sql-api.c ++++ b/src/lib-sql/sql-api.c +@@ -109,6 +109,20 @@ static struct sql_result *sql_result_new_error(const char *error) + return &result->result; + } + ++static void ++sql_query_callback_delayed(struct sql_db *db, struct sql_result *result, ++ sql_query_callback_t *callback, void *context) ++{ ++ struct sql_query_result_delayed *cb = ++ i_new(struct sql_query_result_delayed, 1); ++ cb->db = db; ++ cb->result = result; ++ cb->callback = callback; ++ cb->context = context; ++ cb->to = timeout_add_short(0, sql_query_delayed_callback, cb); ++ DLLIST_PREPEND(&db->query_delayed_list, cb); ++} ++ + void sql_drivers_init_without_drivers(void) + { + i_array_init(&sql_drivers, 8); +@@ -347,13 +361,8 @@ void sql_query(struct sql_db *db, const char *query, + return; + } + +- struct sql_query_result_delayed *cb = i_new(struct sql_query_result_delayed, 1); +- cb->db = db; +- cb->result = sql_query_s(db, query); +- cb->callback = callback; +- cb->context = context; +- cb->to = timeout_add_short(0, sql_query_delayed_callback, cb); +- DLLIST_PREPEND(&db->query_delayed_list, cb); ++ sql_query_callback_delayed(db, sql_query_s(db, query), ++ callback, context); + } + + struct sql_result *sql_query_s(struct sql_db *db, const char *query) +-- +2.39.5 + diff --git a/patches/0002-lib-storage-Remove-MAILBOX_FLAG_AUTO_CREATE-and-MAIL.patch b/patches/0002-lib-storage-Remove-MAILBOX_FLAG_AUTO_CREATE-and-MAIL.patch new file mode 100644 index 0000000..64dd4fd --- /dev/null +++ b/patches/0002-lib-storage-Remove-MAILBOX_FLAG_AUTO_CREATE-and-MAIL.patch @@ -0,0 +1,70 @@ +From e6c952b3ab417f97e9bbc3534b3e40131e808b1e Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 11 Jun 2026 17:20:40 +0000 +Subject: [PATCH 2/2] lib-storage: Remove MAILBOX_FLAG_AUTO_CREATE and + MAILBOX_FLAG_AUTO_SUBSCRIBE + +These flags were set only by mail delivery (lib-lda) and Sieve fileinto +(Pigeonhole), both of which now create the target mailbox explicitly with +mailbox_create(). Nothing sets the flags anymore, so remove them and drop +the flag checks from mailbox_is_autocreated() and mailbox_is_autosubscribed(). + +As a result mailbox_is_autocreated() reflects only mailboxes that virtually +already exist (the user's INBOX and admin-configured "auto" mailboxes), so +the ACL plugin again enforces the CREATE right for delivery-autocreated +mailboxes in shared and public namespaces. +--- + src/lib-storage-lua/mail-storage-lua.c | 2 -- + src/lib-storage/mail-storage.c | 4 ---- + src/lib-storage/mail-storage.h | 4 ---- + 3 files changed, 10 deletions(-) + +Index: trixie/src/lib-storage/mail-storage.c +=================================================================== +--- trixie.orig/src/lib-storage/mail-storage.c ++++ trixie/src/lib-storage/mail-storage.c +@@ -1375,16 +1375,12 @@ bool mailbox_is_autocreated(struct mailb + { + if (box->inbox_user) + return TRUE; +- if ((box->flags & MAILBOX_FLAG_AUTO_CREATE) != 0) +- return TRUE; + return box->set != NULL && + strcmp(box->set->autocreate, MAILBOX_SET_AUTO_NO) != 0; + } + + bool mailbox_is_autosubscribed(struct mailbox *box) + { +- if ((box->flags & MAILBOX_FLAG_AUTO_SUBSCRIBE) != 0) +- return TRUE; + return box->set != NULL && + strcmp(box->set->autocreate, MAILBOX_SET_AUTO_SUBSCRIBE) == 0; + } +Index: trixie/src/lib-storage/mail-storage.h +=================================================================== +--- trixie.orig/src/lib-storage/mail-storage.h ++++ trixie/src/lib-storage/mail-storage.h +@@ -74,10 +74,6 @@ enum mailbox_flags { + quota updates (possibly resulting in broken quota). and This is + useful for example when deleting entire user accounts. */ + MAILBOX_FLAG_DELETE_UNSAFE = 0x400, +- /* Mailbox is created implicitly if it does not exist. */ +- MAILBOX_FLAG_AUTO_CREATE = 0x1000, +- /* Mailbox is subscribed to implicitly when it is created automatically */ +- MAILBOX_FLAG_AUTO_SUBSCRIBE = 0x2000, + /* Run fsck for mailbox index before doing anything else. This may be + useful in fixing index corruption errors that aren't otherwise + detected and that are causing the full mailbox opening to fail. */ +Index: trixie/src/lib-storage/mail-storage-lua.c +=================================================================== +--- trixie.orig/src/lib-storage/mail-storage-lua.c ++++ trixie/src/lib-storage/mail-storage-lua.c +@@ -33,8 +33,6 @@ static struct dlua_table_values lua_stor + DLUA_TABLE_ENUM(MAILBOX_FLAG_NO_INDEX_FILES), + DLUA_TABLE_ENUM(MAILBOX_FLAG_KEEP_LOCKED), + DLUA_TABLE_ENUM(MAILBOX_FLAG_IGNORE_ACLS), +- DLUA_TABLE_ENUM(MAILBOX_FLAG_AUTO_CREATE), +- DLUA_TABLE_ENUM(MAILBOX_FLAG_AUTO_SUBSCRIBE), + + DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_FULL_READ), + DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_FAST), diff --git a/patches/0002-lib-storage-Simplify-mailbox_mkdir-using-mailbox_lis.patch b/patches/0002-lib-storage-Simplify-mailbox_mkdir-using-mailbox_lis.patch new file mode 100644 index 0000000..de6e082 --- /dev/null +++ b/patches/0002-lib-storage-Simplify-mailbox_mkdir-using-mailbox_lis.patch @@ -0,0 +1,31 @@ +From 01b4ddd1de4b5bfd499891f4b3592880b02c3854 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 23 Jun 2026 15:26:51 +0000 +Subject: [PATCH 2/2] lib-storage: Simplify mailbox_mkdir() using + mailbox_list_mkdir_root(NULL) + +mailbox_mkdir() looked up the root path via mailbox_list_get_root_forced() +only to pass it straight to mailbox_list_mkdir_root(). Let mkdir_root() +resolve the root itself by passing path=NULL, dropping the local variable. +--- + src/lib-storage/mail-storage.c | 4 +--- + 1 file changed, 1 insertion(+), 3 deletions(-) + +Index: trixie/src/lib-storage/mail-storage.c +=================================================================== +--- trixie.orig/src/lib-storage/mail-storage.c ++++ trixie/src/lib-storage/mail-storage.c +@@ -3441,12 +3441,10 @@ int mailbox_mkdir(struct mailbox *box, c + enum mailbox_list_path_type type) + { + const struct mailbox_permissions *perm = mailbox_get_permissions(box); +- const char *root_dir; + + if (!perm->gid_origin_is_mailbox_path) { + /* mailbox root directory doesn't exist, create it */ +- root_dir = mailbox_list_get_root_forced(box->list, type); +- if (mailbox_list_mkdir_root(box->list, root_dir, type) < 0) { ++ if (mailbox_list_mkdir_root(box->list, NULL, type) < 0) { + mail_storage_copy_list_error(box->storage, box->list); + return -1; + } diff --git a/patches/0002-lib-storage-thread-Limit-References-header-msgid-cou.patch b/patches/0002-lib-storage-thread-Limit-References-header-msgid-cou.patch new file mode 100644 index 0000000..bbe38e1 --- /dev/null +++ b/patches/0002-lib-storage-thread-Limit-References-header-msgid-cou.patch @@ -0,0 +1,47 @@ +From fb14defe1804881a1df50c0b29679f9fdf8ff5b0 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 16 Apr 2026 16:26:47 +0000 +Subject: [PATCH 2/3] lib-storage: thread - Limit References: header msgid + count to prevent O(N^2) CPU usage + +Cap per-message References ingestion at MAIL_THREAD_REFERENCES_MAX (1000) +in mail_thread_map_add_mail(). Without this limit a single crafted email +with N unique Message-IDs triggers N(N-1)/2 ancestor traversals in +thread_node_has_ancestor(), allowing unauthenticated DoS via mail delivery. +--- + src/lib-storage/index/index-thread-private.h | 4 ++++ + src/lib-storage/index/index-thread.c | 3 ++- + 2 files changed, 6 insertions(+), 1 deletion(-) + +diff --git a/src/lib-storage/index/index-thread-private.h b/src/lib-storage/index/index-thread-private.h +index ed2837b7bb..563c3a3d2d 100644 +--- a/src/lib-storage/index/index-thread-private.h ++++ b/src/lib-storage/index/index-thread-private.h +@@ -22,6 +22,10 @@ + #define MAIL_THREAD_NODE_REF_MSGID 0 + #define MAIL_THREAD_NODE_REF_INREPLYTO 1 + #define MAIL_THREAD_NODE_REF_REFERENCES1 2 ++/* Maximum number of References: header message-IDs stored per message. ++ Prevents O(N²) CPU usage in thread_node_has_ancestor() when a single ++ crafted message contains an abnormally long reference chain. */ ++#define MAIL_THREAD_REFERENCES_MAX 1000 + + struct mail_thread_node { + /* UID of the message, or 0 for dummy nodes */ +diff --git a/src/lib-storage/index/index-thread.c b/src/lib-storage/index/index-thread.c +index e20a97072c..a58f9681f3 100644 +--- a/src/lib-storage/index/index-thread.c ++++ b/src/lib-storage/index/index-thread.c +@@ -280,7 +280,8 @@ mail_thread_map_add_mail(struct mail_thread_context *ctx, struct mail *mail) + ref_index, msgid); + } + } T_END; +- } while (msgid != NULL); ++ } while (msgid != NULL && ++ ref_index < MAIL_THREAD_NODE_REF_REFERENCES1 + MAIL_THREAD_REFERENCES_MAX); + } else { + /* no References:, use In-Reply-To: */ + if (thread_get_mail_header(mail, HDR_IN_REPLY_TO, +-- +2.47.3 + diff --git a/patches/0002-login-common-Avoid-array_front-panic-on-empty-forwar.patch b/patches/0002-login-common-Avoid-array_front-panic-on-empty-forwar.patch new file mode 100644 index 0000000..36f45c6 --- /dev/null +++ b/patches/0002-login-common-Avoid-array_front-panic-on-empty-forwar.patch @@ -0,0 +1,50 @@ +From 54211965c41eb15290bae017b8acfd95d08e0a9e Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sun, 3 May 2026 16:24:38 +0000 +Subject: [PATCH 2/2] login-common: Avoid array_front() panic on empty + forward_fields array + +sasl_server_auth_request_info_fill() and proxy_redirect_reauth() +NUL-terminate the forward_fields array via array_append_zero() + +array_pop_back() and then call array_front() to hand the C array to +the auth client. array_front() asserts when the array is empty, so a +created-but-empty forward_fields array crashes login. + +The empty-array case is no longer reachable from +client_forward_decode_base64() after the previous commit, but guard +defensively here as well: any future caller that creates the array +without populating it should not be able to panic the process. +--- + src/login-common/client-common-auth.c | 2 +- + src/login-common/sasl-server.c | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/login-common/client-common-auth.c b/src/login-common/client-common-auth.c +index 0b226497c6..36ed7211a7 100644 +--- a/src/login-common/client-common-auth.c ++++ b/src/login-common/client-common-auth.c +@@ -490,7 +490,7 @@ proxy_redirect_reauth(struct client *client, const char *destuser, + t_array_init(&info.extra_fields, N_ELEMENTS(extra_fields)); + array_append(&info.extra_fields, extra_fields, + N_ELEMENTS(extra_fields)); +- if (array_is_created(&client->forward_fields)) { ++ if (array_not_empty(&client->forward_fields)) { + array_append_zero(&client->forward_fields); + array_pop_back(&client->forward_fields); + info.forward_fields = array_front(&client->forward_fields); +diff --git a/src/login-common/sasl-server.c b/src/login-common/sasl-server.c +index f02262c00f..88d46e7c68 100644 +--- a/src/login-common/sasl-server.c ++++ b/src/login-common/sasl-server.c +@@ -534,7 +534,7 @@ int sasl_server_auth_request_info_fill(struct client *client, + info_r->real_remote_port = client->real_remote_port; + if (client->client_id != NULL) + info_r->client_id = str_c(client->client_id); +- if (array_is_created(&client->forward_fields)) { ++ if (array_not_empty(&client->forward_fields)) { + array_append_zero(&client->forward_fields); + array_pop_back(&client->forward_fields); + info_r->forward_fields = array_front(&client->forward_fields); +-- +2.47.3 + diff --git a/patches/0002-managesieve-login-client_skip_line-Discard-data-when.patch b/patches/0002-managesieve-login-client_skip_line-Discard-data-when.patch new file mode 100644 index 0000000..22bd2b2 --- /dev/null +++ b/patches/0002-managesieve-login-client_skip_line-Discard-data-when.patch @@ -0,0 +1,29 @@ +From eee1ecfc512d348f8fb60633b6bd26b1536319da Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 25 May 2026 10:38:13 +0000 +Subject: [PATCH 2/2] managesieve-login: client_skip_line() - Discard data when + newline is not found + +Without this, data without a newline (e.g., from a lone CR protocol violation +detected by the parser) stays in the buffer indefinitely. This prevents the +server from ever detecting client disconnect, causing a deadlock where both +sides wait for the other. + +The post-login server-side client_skip_line() in managesieve-client.c already +handles this correctly by always calling i_stream_skip(). +--- + src/managesieve-login/client.c | 1 + + 1 file changed, 1 insertion(+) + +Index: trixie/pigeonhole/src/managesieve-login/client.c +=================================================================== +--- trixie.orig/pigeonhole/src/managesieve-login/client.c ++++ trixie/pigeonhole/src/managesieve-login/client.c +@@ -51,6 +51,7 @@ bool client_skip_line(struct managesieve + } + } + ++ i_stream_skip(client->input, data_size); + return FALSE; + } + diff --git a/patches/0002-submission-login-client-Fix-panic-occurring-at-mail_.patch b/patches/0002-submission-login-client-Fix-panic-occurring-at-mail_.patch new file mode 100644 index 0000000..a35ee92 --- /dev/null +++ b/patches/0002-submission-login-client-Fix-panic-occurring-at-mail_.patch @@ -0,0 +1,78 @@ +From f0bb034096518187b2b97ab25cd62a6d5517a150 Mon Sep 17 00:00:00 2001 +From: Stephan Bosch +Date: Fri, 28 Nov 2025 04:12:10 +0100 +Subject: [PATCH 2/5] submission-login: client - Fix panic occurring at + mail_max_userip_connections limit transgression + +Panic was: +Panic: epoll_ctl(del, 8) failed: Bad file descriptor + +Fixed by making sure the underlying smtp-server connection is always closed before the connection FD is closed. +--- + src/lib-smtp/smtp-server-connection.c | 5 +++++ + src/lib-smtp/smtp-server.h | 1 + + src/submission-login/client.c | 16 ++++++++++++++++ + 3 files changed, 22 insertions(+) + +Index: trixie/src/lib-smtp/smtp-server-connection.c +=================================================================== +--- trixie.orig/src/lib-smtp/smtp-server-connection.c ++++ trixie/src/lib-smtp/smtp-server-connection.c +@@ -1401,6 +1401,11 @@ void smtp_server_connection_close(struct + smtp_server_connection_unref(&conn); + } + ++bool smtp_server_connection_is_closed(struct smtp_server_connection *conn) ++{ ++ return conn->closed; ++} ++ + void smtp_server_connection_terminate(struct smtp_server_connection **_conn, + const char *enh_code, const char *reason) + { +Index: trixie/src/lib-smtp/smtp-server.h +=================================================================== +--- trixie.orig/src/lib-smtp/smtp-server.h ++++ trixie/src/lib-smtp/smtp-server.h +@@ -476,6 +476,7 @@ void smtp_server_connection_set_ssl_stre + + void smtp_server_connection_close(struct smtp_server_connection **_conn, + const char *reason) ATTR_NULL(2); ++bool smtp_server_connection_is_closed(struct smtp_server_connection *conn); + void smtp_server_connection_terminate(struct smtp_server_connection **_conn, + const char *enh_code, const char *reason) + ATTR_NULL(3); +Index: trixie/src/submission-login/client.c +=================================================================== +--- trixie.orig/src/submission-login/client.c ++++ trixie/src/submission-login/client.c +@@ -146,6 +146,21 @@ static int submission_client_create(stru + return 0; + } + ++static void ++submission_client_disconnect(struct client *client, const char *reason) ++{ ++ struct submission_client *subm_client = ++ container_of(client, struct submission_client, common); ++ ++ /* If the smtp-server connection is already in its close cascade (i.e. ++ we're being called via its conn_disconnect callback), skip closing ++ it again. Doing so would null subm_client->conn and prevent the ++ conn_free callback from invoking client_destroy(). */ ++ if (subm_client->conn != NULL && ++ !smtp_server_connection_is_closed(subm_client->conn)) ++ smtp_server_connection_close(&subm_client->conn, reason); ++} ++ + static void submission_client_destroy(struct client *client) + { + struct submission_client *subm_client = +@@ -320,6 +335,7 @@ static const struct smtp_server_callback + static struct client_vfuncs submission_client_vfuncs = { + .alloc = submission_client_alloc, + .create = submission_client_create, ++ .disconnect = submission_client_disconnect, + .destroy = submission_client_destroy, + .reload_config = submission_client_reload_config, + .notify_auth_ready = submission_client_notify_auth_ready, diff --git a/patches/0003-auth-db-oauth2-Add-oauth2_audience-setting-deprecate.patch b/patches/0003-auth-db-oauth2-Add-oauth2_audience-setting-deprecate.patch new file mode 100644 index 0000000..8ef24a4 --- /dev/null +++ b/patches/0003-auth-db-oauth2-Add-oauth2_audience-setting-deprecate.patch @@ -0,0 +1,131 @@ +From fd7d4fddf1798b118b0c5481a0dabe03250829ca Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Fri, 29 May 2026 07:31:50 +0000 +Subject: [PATCH 3/4] auth: db-oauth2: Add oauth2_audience setting, deprecate + aud-as-scope fallback + +Add a dedicated oauth2_audience setting checked against the token's aud +claim (RFC 7519 section 4.1.3) using the same tab-split AND-semantics as +oauth2_scope. Emit a deprecation warning when the existing aud fallback +in db_oauth2_token_in_scope() is triggered so operators know to migrate. +--- + src/auth/db-oauth2.c | 53 ++++++++++++++++++++++++++++++++++++++++++-- + src/auth/db-oauth2.h | 2 ++ + 2 files changed, 53 insertions(+), 2 deletions(-) + +Index: trixie/src/auth/db-oauth2.c +=================================================================== +--- trixie.orig/src/auth/db-oauth2.c ++++ trixie/src/auth/db-oauth2.c +@@ -17,16 +17,21 @@ + #include "db-oauth2.h" + #include "dcrypt.h" + #include "dict.h" ++#include + + #undef DEF + #define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type("oauth2_"#name, name, struct auth_oauth2_settings) + ++#define WARN_AUD_FALLBACK_PERIOD 600 ++static time_t last_warned_aud_fallback = 0; ++ + static const struct setting_define auth_oauth2_setting_defines[] = { + DEF(STR, tokeninfo_url), + DEF(STR, grant_url), + DEF(STR, introspection_url), + DEF(BOOLLIST, scope), ++ DEF(BOOLLIST, audience), + DEF(ENUM, introspection_mode), + DEF(STR_NOVARS, username_validation_format), + DEF(STR, username_attribute), +@@ -50,6 +55,7 @@ static const struct auth_oauth2_settings + .grant_url = "", + .introspection_url = "", + .scope = ARRAY_INIT, ++ .audience = ARRAY_INIT, + .force_introspection = FALSE, + .introspection_mode = ":auth:get:post:local", + .username_validation_format = "%{user}", +@@ -558,8 +564,19 @@ db_oauth2_token_in_scope(struct db_oauth + + const char *value = auth_fields_find(req->fields, "scope"); + bool has_scope = value != NULL; +- if (!has_scope) ++ if (!has_scope && array_is_empty(&req->db->set->audience)) { + value = auth_fields_find(req->fields, "aud"); ++ if (value != NULL) { ++ time_t t0 = time(NULL); ++ if (t0 - last_warned_aud_fallback > WARN_AUD_FALLBACK_PERIOD) { ++ e_warning(authdb_event(req->auth_request), ++ "Token has no 'scope' claim; falling back to " ++ "'aud' for scope check is deprecated - " ++ "use oauth2_audience instead"); ++ last_warned_aud_fallback = t0; ++ } ++ } ++ } + e_debug(authdb_event(req->auth_request), + "Token scope(s): %s", value); + +@@ -585,6 +602,37 @@ db_oauth2_token_in_scope(struct db_oauth + return found; + } + ++static bool ++db_oauth2_token_in_audience(struct db_oauth2_request *req, ++ enum passdb_result *result_r, const char **error_r) ++{ ++ if (array_is_empty(&req->db->set->audience)) ++ return TRUE; ++ ++ const char *value = auth_fields_find(req->fields, "aud"); ++ e_debug(authdb_event(req->auth_request), ++ "Token audience(s): %s", value != NULL ? value : "(none)"); ++ ++ bool found = FALSE; ++ if (value != NULL && *value != '\0') { ++ const char *const *entries = t_strsplit_tabescaped(value); ++ const char *wanted; ++ found = TRUE; ++ array_foreach_elem(&req->db->set->audience, wanted) { ++ if (!str_array_find(entries, wanted)) { ++ found = FALSE; ++ break; ++ } ++ } ++ } ++ if (!found) { ++ *error_r = t_strdup_printf("Token audience does not include '%s'", ++ t_array_const_string_join(&req->db->set->audience, " ")); ++ *result_r = PASSDB_RESULT_USER_DISABLED; ++ } ++ return found; ++} ++ + static void db_oauth2_process_fields(struct db_oauth2_request *req, + enum passdb_result *result_r, + const char **error_r) +@@ -593,7 +641,8 @@ static void db_oauth2_process_fields(str + + if (db_oauth2_user_is_enabled(req, result_r, error_r) && + db_oauth2_validate_username(req, result_r, error_r) && +- db_oauth2_token_in_scope(req, result_r, error_r)) { ++ db_oauth2_token_in_scope(req, result_r, error_r) && ++ db_oauth2_token_in_audience(req, result_r, error_r)) { + /* The user has now been successfully authenticated, + mark the request as such. This allows having no + passdb in config. */ +Index: trixie/src/auth/db-oauth2.h +=================================================================== +--- trixie.orig/src/auth/db-oauth2.h ++++ trixie/src/auth/db-oauth2.h +@@ -15,6 +15,8 @@ struct auth_oauth2_settings { + const char *introspection_url; + /* expected scope(s), optional */ + ARRAY_TYPE(const_string) scope; ++ /* expected audience(s) (aud claim), optional */ ++ ARRAY_TYPE(const_string) audience; + /* mode of introspection, one of auth, get, post, local + - auth: send token with header Authorization: Bearer token + - get: append token to url diff --git a/patches/0003-doveadm-compress-Delay-failing-if-used-COMPRESS-algo.patch b/patches/0003-doveadm-compress-Delay-failing-if-used-COMPRESS-algo.patch new file mode 100644 index 0000000..f34397f --- /dev/null +++ b/patches/0003-doveadm-compress-Delay-failing-if-used-COMPRESS-algo.patch @@ -0,0 +1,41 @@ +From 161256a377c194aa8e63c83053c35cdd1ab75eda Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 7 Jul 2026 14:33:40 +0300 +Subject: [PATCH 3/3] doveadm compress: Delay failing if used COMPRESS + algorithm is not supported locally + +This command is used in CI tests to verify the server rejects non-DEFLATE +algorithm. +--- + src/doveadm/doveadm-compress.c | 9 ++++++++- + 1 file changed, 8 insertions(+), 1 deletion(-) + +Index: trixie/src/doveadm/doveadm-compress.c +=================================================================== +--- trixie.orig/src/doveadm/doveadm-compress.c ++++ trixie/src/doveadm/doveadm-compress.c +@@ -123,9 +123,14 @@ client_input_get_compress_algorithm(stru + if (!str_begins_icase(line, " COMPRESS ", &algorithm)) + return FALSE; + ++ /* Look up the local handler needed to (de)compress the stream once the ++ server accepts. Don't fail here if the mechanism is unknown or not ++ compiled in: the server may reject it, and this client must still send ++ the command to let the server do so. If the server unexpectedly ++ accepts a mechanism we can't handle, we fail then (see server_input()). */ + if (compression_lookup_handler(t_str_lcase(algorithm), + &client->handler) <= 0) +- i_fatal("Unsupported compression mechanism: %s", algorithm); ++ client->handler = NULL; + /* Remember the tag so we can tell whether the server accepted or + rejected this COMPRESS command. */ + i_free(client->compress_tag); +@@ -281,6 +286,8 @@ static void server_input(struct client * + struct istream *input; + struct ostream *output; + ++ if (client->handler == NULL) ++ i_fatal("Server accepted unsupported compression mechanism"); + e_info(client->event, ""); + input = client->handler->create_istream(client->input); + output = client->handler-> diff --git a/patches/0003-imap-Stream-ENVELOPE-to-client-without-ostream-memor.patch b/patches/0003-imap-Stream-ENVELOPE-to-client-without-ostream-memor.patch new file mode 100644 index 0000000..aafce22 --- /dev/null +++ b/patches/0003-imap-Stream-ENVELOPE-to-client-without-ostream-memor.patch @@ -0,0 +1,75 @@ +From e14d6ae2825adf24f58e397e3cf88d8c9f41e8a2 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 17 Apr 2026 16:55:34 +0200 +Subject: [PATCH 03/14] imap: Stream ENVELOPE to client without ostream memory + duplication + +Same fix as for BODYSTRUCTURE: avoid copying the envelope string into +the ostream ring buffer by using o_stream_set_max_buffer_size(0) + +o_stream_send_istream() with a continuation handler. + +Co-Authored-By: Claude Sonnet 4.6 +--- + src/imap/imap-fetch.c | 38 ++++++++++++++++++++++++++++++++++---- + 1 file changed, 34 insertions(+), 4 deletions(-) + +diff --git a/src/imap/imap-fetch.c b/src/imap/imap-fetch.c +index b4a8a465d7..5a6629916b 100644 +--- a/src/imap/imap-fetch.c ++++ b/src/imap/imap-fetch.c +@@ -793,6 +793,34 @@ static bool fetch_bodystructure_init(struct imap_fetch_init_context *ctx) + return TRUE; + } + ++static int fetch_envelope_stream_continue(struct imap_fetch_context *ctx) ++{ ++ struct imap_fetch_state *state = &ctx->state; ++ enum ostream_send_istream_result res; ++ ++ o_stream_set_max_buffer_size(ctx->client->output, 0); ++ res = o_stream_send_istream(ctx->client->output, state->cur_input); ++ o_stream_set_max_buffer_size(ctx->client->output, SIZE_MAX); ++ ++ switch (res) { ++ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED: ++ i_stream_unref(&state->cur_input); ++ state->cont_handler = NULL; ++ if (o_stream_send(ctx->client->output, ")", 1) < 0) ++ return -1; ++ return 1; ++ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT: ++ i_unreached(); ++ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT: ++ return 0; ++ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT: ++ i_unreached(); ++ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT: ++ return -1; ++ } ++ i_unreached(); ++} ++ + static int fetch_envelope(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) + { +@@ -808,11 +836,13 @@ static int fetch_envelope(struct imap_fetch_context *ctx, struct mail *mail, + return -1; + } + +- if (o_stream_send(ctx->client->output, "ENVELOPE (", 10) < 0 || +- o_stream_send_str(ctx->client->output, envelope) < 0 || +- o_stream_send(ctx->client->output, ")", 1) < 0) ++ if (o_stream_send(ctx->client->output, "ENVELOPE (", 10) < 0) + return -1; +- return 1; ++ ++ ctx->state.cur_input = ++ i_stream_create_from_data(envelope, strlen(envelope)); ++ ctx->state.cont_handler = fetch_envelope_stream_continue; ++ return fetch_envelope_stream_continue(ctx); + } + + static bool fetch_envelope_init(struct imap_fetch_init_context *ctx) +-- +2.47.3 + diff --git a/patches/0003-lib-Add-str_stable_hash.patch b/patches/0003-lib-Add-str_stable_hash.patch new file mode 100644 index 0000000..a807e27 --- /dev/null +++ b/patches/0003-lib-Add-str_stable_hash.patch @@ -0,0 +1,43 @@ +From 4778fe2357a1fcaa45d566f5095e80d8be8f4e4b Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 13 Apr 2026 17:20:46 +0200 +Subject: [PATCH 03/12] lib: Add str_stable_hash() + +For now this is the same as str_hash(), but this is changed in a following +commit. +--- + src/lib/hash.c | 5 +++++ + src/lib/hash.h | 1 + + 2 files changed, 6 insertions(+) + +diff --git a/src/lib/hash.c b/src/lib/hash.c +index 8f4cd7d9b4..61fe86ed8e 100644 +--- a/src/lib/hash.c ++++ b/src/lib/hash.c +@@ -525,6 +525,11 @@ unsigned int str_hash(const char *p) + return xxh64_to_32(xxh64_data(p, strlen(p), 0)); + } + ++unsigned int str_stable_hash(const char *p) ++{ ++ return xxh64_to_32(xxh64_data(p, strlen(p), 0)); ++} ++ + unsigned int strcase_hash(const char *p) + { + struct xxh64_context ctx; +diff --git a/src/lib/hash.h b/src/lib/hash.h +index 5716a77a7d..84d9c52aef 100644 +--- a/src/lib/hash.h ++++ b/src/lib/hash.h +@@ -171,6 +171,7 @@ void hash_table_copy(struct hash_table *dest, struct hash_table *src); + /* hash function for strings */ + unsigned int str_hash(const char *p) ATTR_PURE; + unsigned int strcase_hash(const char *p) ATTR_PURE; ++unsigned int str_stable_hash(const char *p) ATTR_PURE; + + /* fast hash function which uppercases a-z. Does not work well + with input that consists from non number/letter input, as +-- +2.47.3 + diff --git a/patches/0003-lib-Add-t_openat_safe_dir.patch b/patches/0003-lib-Add-t_openat_safe_dir.patch new file mode 100644 index 0000000..63cc4fe --- /dev/null +++ b/patches/0003-lib-Add-t_openat_safe_dir.patch @@ -0,0 +1,129 @@ +From ba9ae23ee80dece8ebc3bb6b28c42b35002afb96 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 3 Jun 2026 18:10:36 +0300 +Subject: [PATCH 3/5] lib: Add t_openat_safe_dir() + +--- + src/lib/path-util.c | 19 +++++++++++++++ + src/lib/path-util.h | 7 ++++++ + src/lib/test-path-util.c | 52 ++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 78 insertions(+) + +Index: trixie/src/lib/path-util.c +=================================================================== +--- trixie.orig/src/lib/path-util.c ++++ trixie/src/lib/path-util.c +@@ -528,6 +528,25 @@ int t_openat_safe(int base_fd, const cha + return path_openat_safe_walk(base_fd, path, flags, error_r); + } + ++int t_openat_safe_dir(const char *base_dir, const char *path, int flags, ++ const char **error_r) ++{ ++ i_assert(base_dir != NULL); ++ i_assert(path != NULL); ++ i_assert(error_r != NULL); ++ ++ int dir_fd = open(base_dir, ++ O_DIRECTORY | O_RDONLY | O_CLOEXEC | ++ (flags & O_NOFOLLOW)); ++ if (dir_fd < 0) { ++ *error_r = t_strdup_printf("open(%s) failed: %m", base_dir); ++ return -1; ++ } ++ int fd = t_openat_safe(dir_fd, path, flags, error_r); ++ i_close_fd(&dir_fd); ++ return fd; ++} ++ + bool t_binary_abspath(const char **binpath, const char **error_r) + { + const char *path_env, *const *paths; +Index: trixie/src/lib/path-util.h +=================================================================== +--- trixie.orig/src/lib/path-util.h ++++ trixie/src/lib/path-util.h +@@ -86,6 +86,13 @@ int t_readlink(const char *path, const c + int t_openat_safe(int base_fd, const char *path, int flags, + const char **error_r); + ++/* Convenience wrapper: opens base_dir as a directory fd, then delegates to ++ t_openat_safe(). If O_NOFOLLOW is present in flags it is also applied to ++ the open() of base_dir itself, refusing a symlink as its final component. ++ Returns the new fd on success, -1 on failure. */ ++int t_openat_safe_dir(const char *base_dir, const char *path, int flags, ++ const char **error_r); ++ + /* Update binpath to be absolute: + * a) begins with '/' -> no change + * b) contains '/' -> assume relative to working directory +Index: trixie/src/lib/test-path-util.c +=================================================================== +--- trixie.orig/src/lib/test-path-util.c ++++ trixie/src/lib/test-path-util.c +@@ -356,6 +356,57 @@ static void test_openat_safe(void) + i_error("rmdir(%s) failed: %m", subdir); + } + ++static void test_openat_safe_dir(void) ++{ ++ const char *error; ++ int fd; ++ ++ /* Plain file: success. */ ++ const char *plain = t_strconcat(tmpdir, "/plain2", NULL); ++ int wfd = creat(plain, 0600); ++ if (wfd < 0) ++ i_fatal("creat(%s) failed: %m", plain); ++ i_close_fd(&wfd); ++ ++ fd = t_openat_safe_dir(tmpdir, "plain2", O_RDONLY, &error); ++ test_assert(fd >= 0); ++ i_close_fd(&fd); ++ ++ /* Nonexistent base_dir. */ ++ errno = 0; ++ fd = t_openat_safe_dir(t_strconcat(tmpdir, "/no-such-dir", NULL), ++ "plain2", O_RDONLY, &error); ++ test_assert(fd == -1); ++ test_assert(errno == ENOENT); ++ test_assert(error != NULL); ++ ++ /* Absolute symlink in relative part: refused. */ ++ const char *abs_link2 = t_strconcat(tmpdir, "/abs-link2", NULL); ++ if (symlink("/etc/hostname", abs_link2) < 0) ++ i_fatal("symlink failed: %m"); ++ errno = 0; ++ fd = t_openat_safe_dir(tmpdir, "abs-link2", O_RDONLY, &error); ++ test_assert(fd == -1); ++ test_assert(errno == ELOOP); ++ ++ /* O_NOFOLLOW: symlink as base_dir final component refused. */ ++ const char *dir_link = t_strconcat(tmpdir, "/dir-link", NULL); ++ if (symlink(tmpdir, dir_link) < 0) ++ i_fatal("symlink failed: %m"); ++ errno = 0; ++ fd = t_openat_safe_dir(dir_link, "plain2", ++ O_RDONLY | O_NOFOLLOW, &error); ++ test_assert(fd == -1); ++ /* Without O_NOFOLLOW the same symlinked base_dir succeeds. */ ++ fd = t_openat_safe_dir(dir_link, "plain2", O_RDONLY, &error); ++ test_assert(fd >= 0); ++ i_close_fd(&fd); ++ ++ i_unlink(plain); ++ i_unlink(abs_link2); ++ i_unlink(dir_link); ++} ++ + static void test_cleanup(void) + { + const char *error; +@@ -396,6 +447,7 @@ void test_path_util(void) + test_link_alloc(); + test_link_alloc2(); + test_openat_safe(); ++ test_openat_safe_dir(); + test_cleanup(); + alarm(0); + test_end(); diff --git a/patches/0003-lib-compression-Add-o_stream_deflate_reset_dict.patch b/patches/0003-lib-compression-Add-o_stream_deflate_reset_dict.patch new file mode 100644 index 0000000..f9e2654 --- /dev/null +++ b/patches/0003-lib-compression-Add-o_stream_deflate_reset_dict.patch @@ -0,0 +1,104 @@ +From c7629782bfbb0f32076c220230db67bd01d5e115 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 16 Apr 2026 17:38:41 +0200 +Subject: [PATCH 3/5] lib-compression: Add o_stream_deflate_reset_dict() + +New public function that locates the deflate ostream in the parent chain +(transparently handles rawlog wrappers) and schedules a Z_FULL_FLUSH on +the next uncork/flush. Z_FULL_FLUSH emits all buffered data and then +resets the deflate dictionary, so subsequent compressed output cannot +reference data from before the call. + +Adds a pending_dict_reset flag to struct zlib_ostream. The flag persists +through partial flush retries and is cleared only once the full flush +loop completes. +--- + src/lib-compression/ostream-zlib.c | 37 ++++++++++++++++++++++++++++-- + src/lib-compression/ostream-zlib.h | 7 ++++++ + 2 files changed, 42 insertions(+), 2 deletions(-) + +diff --git a/src/lib-compression/ostream-zlib.c b/src/lib-compression/ostream-zlib.c +index 6aeccbf35d..5bc41d0b2c 100644 +--- a/src/lib-compression/ostream-zlib.c ++++ b/src/lib-compression/ostream-zlib.c +@@ -23,6 +23,7 @@ struct zlib_ostream { + + bool gz:1; + bool flushed:1; ++ bool pending_dict_reset:1; + }; + + struct zlib_settings { +@@ -252,8 +253,14 @@ o_stream_zlib_send_flush(struct zlib_ostream *zstream, bool final) + if ((ret = o_stream_zlib_send_outbuf(zstream)) <= 0) + return ret; + +- flush = final ? Z_FINISH : +- (!zstream->gz ? Z_SYNC_FLUSH : Z_NO_FLUSH); ++ if (final) ++ flush = Z_FINISH; ++ else if (zstream->gz) ++ flush = Z_NO_FLUSH; ++ else if (zstream->pending_dict_reset) ++ flush = Z_FULL_FLUSH; ++ else ++ flush = Z_SYNC_FLUSH; + + i_assert(zstream->outbuf_used == 0); + do { +@@ -283,6 +290,10 @@ o_stream_zlib_send_flush(struct zlib_ostream *zstream, bool final) + } + } while (zs->avail_out != sizeof(zstream->outbuf)); + ++ /* Z_FULL_FLUSH completes here; clear the request so subsequent flushes ++ use the normal Z_SYNC_FLUSH again. */ ++ zstream->pending_dict_reset = FALSE; ++ + if (final) { + if (o_stream_zlib_send_gz_trailer(zstream) < 0) + return -1; +@@ -443,3 +454,25 @@ struct ostream *o_stream_create_deflate_auto(struct ostream *output, struct even + { + return o_stream_create_zlib_auto(output, event, FALSE); + } ++void o_stream_deflate_reset_dict(struct ostream *_output) ++{ ++ struct ostream_private *stream = _output->real_stream; ++ ++ /* Traverse the ostream parent chain to find the deflate ostream. ++ The caller may pass a rawlog-wrapped stream or the deflate stream ++ itself; either way we find it by its sendv function pointer. */ ++ while (stream != NULL) { ++ if (stream->sendv == o_stream_zlib_sendv) { ++ struct zlib_ostream *zstream = ++ container_of(stream, struct zlib_ostream, ostream); ++ i_assert(!zstream->gz); ++ zstream->pending_dict_reset = TRUE; ++ o_stream_set_flush_pending( ++ &zstream->ostream.ostream, TRUE); ++ return; ++ } ++ stream = (stream->parent != NULL) ? ++ stream->parent->real_stream : NULL; ++ } ++ /* No deflate ostream found in the chain – nothing to reset. */ ++} +diff --git a/src/lib-compression/ostream-zlib.h b/src/lib-compression/ostream-zlib.h +index 9738e36cc0..4db62fda81 100644 +--- a/src/lib-compression/ostream-zlib.h ++++ b/src/lib-compression/ostream-zlib.h +@@ -7,4 +7,11 @@ struct ostream *o_stream_create_bz2_auto(struct ostream *output, struct event *e + struct ostream *o_stream_create_lz4_auto(struct ostream *output, struct event *event); + struct ostream *o_stream_create_zstd_auto(struct ostream *output, struct event *event); + ++/* Reset the deflate compression dictionary so that subsequent compressed ++ output cannot reference data from before this call. Pass any ostream in ++ the chain (e.g. a rawlog wrapper); the function locates the deflate stream ++ internally. Safe to call when compression is not active – it is a no-op ++ in that case. */ ++void o_stream_deflate_reset_dict(struct ostream *output); ++ + #endif +-- +2.47.3 + diff --git a/patches/0003-lib-compression-istream-zstd-Guard-against-no-progre.patch b/patches/0003-lib-compression-istream-zstd-Guard-against-no-progre.patch new file mode 100644 index 0000000..e4fdefa --- /dev/null +++ b/patches/0003-lib-compression-istream-zstd-Guard-against-no-progre.patch @@ -0,0 +1,39 @@ +From cc00fbd91027e9f7b3595ced4c505deec33e7f6c Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Fri, 12 Jun 2026 11:19:42 +0000 +Subject: [PATCH 3/4] lib-compression: istream-zstd - Guard against no-progress + loop in read + +If ZSTD_decompressStream() returns with neither input consumed nor +output produced, the read loop would spin indefinitely. Add a check +after each call: if input.pos is unchanged and output.pos is zero, +treat it as a corrupt stream (EIO). +--- + src/lib-compression/istream-zstd.c | 8 ++++++++ + 1 file changed, 8 insertions(+) + +Index: trixie/src/lib-compression/istream-zstd.c +=================================================================== +--- trixie.orig/src/lib-compression/istream-zstd.c ++++ trixie/src/lib-compression/istream-zstd.c +@@ -175,12 +175,20 @@ static ssize_t i_stream_zstd_read(struct + zstream->output.pos = 0; + zstream->output.size = ZSTD_DStreamOutSize(); + ++ size_t old_input_pos = zstream->input.pos; + size_t zret = ZSTD_decompressStream(zstream->dstream, &zstream->output, + &zstream->input); + if (ZSTD_isError(zret) != 0) { + i_stream_zstd_read_error(zstream, zret); + return -1; + } ++ if (zstream->input.pos == old_input_pos && zstream->output.pos == 0) { ++ io_stream_set_error(&zstream->istream.iostream, ++ "zstd.read(%s): decompressor made no progress", ++ i_stream_get_name(&zstream->istream.istream)); ++ zstream->istream.istream.stream_errno = EIO; ++ return -1; ++ } + /* ZSTD magic number is 4 bytes, but it's only defined after v0.8 */ + if (!zstream->hdr_read && zstream->input.size > 4) + zstream->hdr_read = TRUE; diff --git a/patches/0003-lib-mail-o_stream_dot_sendv-Use-enumeration-to-defin.patch b/patches/0003-lib-mail-o_stream_dot_sendv-Use-enumeration-to-defin.patch new file mode 100644 index 0000000..ecf30b1 --- /dev/null +++ b/patches/0003-lib-mail-o_stream_dot_sendv-Use-enumeration-to-defin.patch @@ -0,0 +1,91 @@ +From a26d37e321c46eb974703ac85bab66cce544d35f Mon Sep 17 00:00:00 2001 +From: Marco Bettini +Date: Fri, 10 Apr 2026 08:49:49 +0000 +Subject: [PATCH 3/4] lib-mail: o_stream_dot_sendv() - Use enumeration to + define the items to inject + +--- + src/lib-mail/ostream-dot.c | 39 ++++++++++++++++++++++++++------------ + 1 file changed, 27 insertions(+), 12 deletions(-) + +Index: trixie/src/lib-mail/ostream-dot.c +=================================================================== +--- trixie.orig/src/lib-mail/ostream-dot.c ++++ trixie/src/lib-mail/ostream-dot.c +@@ -71,6 +71,12 @@ o_stream_dot_close(struct iostream_priva + o_stream_close(dstream->ostream.parent); + } + ++enum o_stream_dot_sendv_add { ++ ADD_NONE, ++ ADD_CR, ++ ADD_DOT, ++}; ++ + static ssize_t + o_stream_dot_sendv(struct ostream_private *stream, + const struct const_iovec *iov, unsigned int iov_count) +@@ -106,7 +112,7 @@ o_stream_dot_sendv(struct ostream_privat + p = data; + pend = CONST_PTR_OFFSET(data, size); + for (; p < pend && ((size_t)(p - data) + 2) <= max_bytes; p++) { +- char add = 0; ++ enum o_stream_dot_sendv_add add = ADD_NONE; + + size = pend - p; + switch (dstream->state) { +@@ -122,8 +128,7 @@ o_stream_dot_sendv(struct ostream_privat + switch (*p) { + case '\n': + dstream->state = STREAM_STATE_CRLF; +- /* add missing CR */ +- add = '\r'; ++ add = ADD_CR; + break; + case '\r': + dstream->state = STREAM_STATE_CR; +@@ -155,12 +160,10 @@ o_stream_dot_sendv(struct ostream_privat + break; + case '\n': + dstream->state = STREAM_STATE_CRLF; +- /* add missing CR */ +- add = '\r'; ++ add = ADD_CR; + break; + case '.': +- /* add dot */ +- add = '.'; ++ add = ADD_DOT; + /* fall through */ + default: + dstream->state = STREAM_STATE_NONE; +@@ -185,12 +188,24 @@ o_stream_dot_sendv(struct ostream_privat + } + /* insert byte (substitute one with pair) */ + data++; +- iovn.iov_base = (add == '\r' ? "\r\n" : ".."); +- iovn.iov_len = 2; ++ ++ switch(add) { ++ case ADD_DOT: ++ iovn.iov_base = ".."; ++ iovn.iov_len = 2; ++ break; ++ case ADD_CR: ++ iovn.iov_base = "\r\n"; ++ iovn.iov_len = 2; ++ break; ++ default: ++ i_unreached(); ++ } ++ + array_push_back(&iov_arr, &iovn); +- i_assert(max_bytes >= 2); +- max_bytes -= 2; +- added++; ++ i_assert(max_bytes >= iovn.iov_len); ++ max_bytes -= iovn.iov_len; ++ added += iovn.iov_len - 1; + sent++; + } + } diff --git a/patches/0003-lib-sieve-Rename-on-disk-header-rusage-fields-to-unu.patch b/patches/0003-lib-sieve-Rename-on-disk-header-rusage-fields-to-unu.patch new file mode 100644 index 0000000..cde5a75 --- /dev/null +++ b/patches/0003-lib-sieve-Rename-on-disk-header-rusage-fields-to-unu.patch @@ -0,0 +1,71 @@ +From e0c8bcd5d0d8bbd39edb2bc78cd15f469834660a Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 3 Jun 2026 12:10:46 +0000 +Subject: [PATCH 3/7] lib-sieve: Rename on-disk header rusage fields to + unused_* + +The .svbin header's resource_usage fields no longer carry data: they +are zeroed on save and discarded on load. Cumulative CPU tracking +lives in the per-user sieve-rusage file, and the in-memory carry-over +lives in sbin->persisted_rusage. + +Flatten the nested resource_usage struct and rename the fields to +unused_update_time and unused_cpu_time_msecs so it is obvious the +fields are kept only for on-disk format compatibility. The memory +layout of struct sieve_binary_header is unchanged. +--- + src/lib-sieve/sieve-binary-file.c | 8 +++++--- + src/lib-sieve/sieve-binary-private.h | 9 +++++---- + 2 files changed, 10 insertions(+), 7 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-file.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c +@@ -147,13 +147,14 @@ sieve_binary_file_read_header(struct sie + return 0; + } + +-/* The on-disk binary header carries an unused resource_usage struct for ++/* The on-disk binary header carries unused resource_usage fields for + backward compatibility. Tracking lives in the per-user sieve-rusage file. */ + static void sieve_binary_file_zero_header_rusage(struct sieve_binary *sbin) + { + struct sieve_binary_header *header = &sbin->header; + +- i_zero(&header->resource_usage); ++ header->unused_update_time = 0; ++ header->unused_cpu_time_msecs = 0; + sieve_resource_usage_init(&sbin->rusage); + sbin->rusage_updated = FALSE; + } +@@ -834,7 +835,8 @@ _sieve_binary_open(struct sieve_binary * + + /* Resource usage tracking is no longer kept in the binary file; + discard any data from older versions. */ +- i_zero(&sbin->header.resource_usage); ++ sbin->header.unused_update_time = 0; ++ sbin->header.unused_cpu_time_msecs = 0; + sbin->header.flags &= ENUM_NEGATE(SIEVE_BINARY_FLAG_RESOURCE_LIMIT); + + /* Load block index */ +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-private.h ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h +@@ -26,10 +26,11 @@ struct sieve_binary_header { + uint32_t hdr_size; + uint32_t flags; + +- struct { +- uint64_t update_time; +- uint32_t cpu_time_msecs; +- } resource_usage; ++ /* Resource usage tracking moved to per-user sieve-rusage file. These ++ fields are kept zeroed and present only for on-disk format ++ compatibility with older .svbin files. */ ++ uint64_t unused_update_time; ++ uint32_t unused_cpu_time_msecs; + }; + + struct sieve_binary_file { diff --git a/patches/0003-lib-sieve-edit_mail_headers_parse-Fix-info-leak-via-.patch b/patches/0003-lib-sieve-edit_mail_headers_parse-Fix-info-leak-via-.patch new file mode 100644 index 0000000..e083d33 --- /dev/null +++ b/patches/0003-lib-sieve-edit_mail_headers_parse-Fix-info-leak-via-.patch @@ -0,0 +1,271 @@ +From 76957df3b7ec3ca682e779fa91e22f6d8bb22740 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 1 May 2026 06:28:33 +0000 +Subject: [PATCH 3/3] lib-sieve: edit_mail_headers_parse() - Fix info leak via + NUL-truncated header copy + +i_strndup() stops at the first NUL byte, so embedded NULs caused field->data +to be shorter than field->size. This left stale heap data readable past the +copied content when callers treat data+body_offset as a string. + +Adds a test-edit-mail unit test and a sieve testsuite case that both +exercise edit_mail_headers_parse() with a NUL byte embedded in a header +value. The unit test memcmp-verifies that the bytes after the NUL are +preserved; the sieve test triggers an invalid heap read detectable by +valgrind under the old code. +--- + Makefile.am | 1 + + src/lib-sieve/util/edit-mail.c | 8 +- + src/lib-sieve/util/test-edit-mail.c | 140 +++++++++++++++++++ + tests/extensions/editheader/nul-value.svtest | 46 ++++++ + 4 files changed, 192 insertions(+), 3 deletions(-) + create mode 100644 tests/extensions/editheader/nul-value.svtest + +Index: trixie/pigeonhole/Makefile.am +=================================================================== +--- trixie.orig/pigeonhole/Makefile.am ++++ trixie/pigeonhole/Makefile.am +@@ -169,6 +169,7 @@ test_cases = \ + tests/extensions/editheader/utf8.svtest \ + tests/extensions/editheader/protected.svtest \ + tests/extensions/editheader/errors.svtest \ ++ tests/extensions/editheader/nul-value.svtest \ + tests/extensions/editheader/execute.svtest \ + tests/extensions/duplicate/errors.svtest \ + tests/extensions/duplicate/execute.svtest \ +Index: trixie/pigeonhole/src/lib-sieve/util/edit-mail.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/util/edit-mail.c ++++ trixie/pigeonhole/src/lib-sieve/util/edit-mail.c +@@ -553,8 +553,9 @@ edit_mail_header_field_create(struct edi + &field->body_offset); + + /* Copy to new field */ +- field->data = i_strndup(str_data(data), str_len(data)); + field->size = str_len(data); ++ field->data = i_malloc(field->size + 1); ++ memcpy(field->data, str_data(data), field->size); + field->virtual_size = (edmail->crlf ? + field->size : field->size + lines); + field->lines = lines; +@@ -831,8 +832,9 @@ static int edit_mail_headers_parse(struc + + field->size = str_len(hdr_data); + field->virtual_size = field->size + vsize_diff; +- field->data = i_strndup(str_data(hdr_data), +- field->size); ++ field->data = i_malloc(field->size + 1); ++ memcpy(field->data, str_data(hdr_data), ++ field->size); + field->offset = offset; + field->lines = lines; + +Index: trixie/pigeonhole/src/lib-sieve/util/test-edit-mail.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/util/test-edit-mail.c ++++ trixie/pigeonhole/src/lib-sieve/util/test-edit-mail.c +@@ -866,6 +866,144 @@ static void test_edit_mail_empty(void) + test_end(); + } + ++static void test_edit_mail_nul_in_header(void) ++{ ++ /* Message with a NUL byte embedded in the X-Has-NUL header value. ++ * sizeof() - 1 gives the true byte count, skipping the C string's ++ * trailing NUL terminator, while preserving the embedded \x00. */ ++ static const unsigned char message[] = ++ "From: sender@example.com\n" ++ "X-Has-NUL: value\x00" "afterNUL\n" ++ "Subject: Test\n" ++ "\n" ++ "Body\n"; ++ /* Expected output after deleting the Subject header. */ ++ static const unsigned char expected[] = ++ "From: sender@example.com\n" ++ "X-Has-NUL: value\x00" "afterNUL\n" ++ "\n" ++ "Body\n"; ++ struct istream *input_msg, *input_mail; ++ buffer_t *buffer; ++ struct mail_raw *rawmail; ++ struct edit_mail *edmail; ++ struct mail *mail; ++ ++ test_begin("edit-mail - NUL byte in header value"); ++ test_edit_mail_init(); ++ ++ /* sizeof - 1 excludes the trailing C-string NUL terminator */ ++ input_msg = i_stream_create_from_data(message, sizeof(message) - 1); ++ ++ rawmail = mail_raw_open_stream(test_raw_mail_user, input_msg); ++ edmail = edit_mail_wrap(rawmail->mail); ++ ++ /* Deleting any header triggers edit_mail_headers_parse(), which with ++ * the old i_strndup() allocated only strlen("X-Has-NUL: value")+1=17 ++ * bytes for X-Has-NUL's field->data even though field->size=26. ++ * Streaming then called memcpy(dst, field->data, 26), reading 9 bytes ++ * past the end of the allocation and producing garbage in place of ++ * "afterNUL\n". */ ++ edit_mail_header_delete(edmail, "Subject", 0); ++ ++ mail = edit_mail_get_mail(edmail); ++ ++ if (mail_get_stream(mail, NULL, NULL, &input_mail) < 0) { ++ i_fatal("Failed to open mail stream: %s", ++ mailbox_get_last_internal_error(mail->box, NULL)); ++ } ++ ++ buffer = buffer_create_dynamic(default_pool, 128); ++ ++ /* normal */ ++ ++ i_stream_seek(input_mail, 0); ++ test_stream_data(input_mail, buffer); ++ ++ test_out("nul in header", ++ buffer->used == sizeof(expected) - 1 && ++ memcmp(buffer->data, expected, sizeof(expected) - 1) == 0); ++ ++ /* slow (byte-by-byte) */ ++ ++ i_stream_seek(input_mail, 0); ++ buffer_set_used_size(buffer, 0); ++ test_stream_data_slow(input_mail, buffer); ++ ++ test_out("nul in header, slow", ++ buffer->used == sizeof(expected) - 1 && ++ memcmp(buffer->data, expected, sizeof(expected) - 1) == 0); ++ ++ /* clean up */ ++ ++ buffer_free(&buffer); ++ edit_mail_unwrap(&edmail); ++ mail_raw_close(&rawmail); ++ i_stream_unref(&input_msg); ++ test_edit_mail_deinit(); ++ test_end(); ++} ++ ++static void test_edit_mail_empty2(void) ++{ ++ struct istream *input_msg, *input_mail; ++ buffer_t *buffer; ++ struct mail_raw *rawmail; ++ struct edit_mail *edmail; ++ struct mail *mail; ++ const char *value; ++ ++ test_begin("edit-mail - empty message (delete, add)"); ++ test_edit_mail_init(); ++ ++ /* Compose the message */ ++ ++ input_msg = i_stream_create_from_data("", 0); ++ ++ rawmail = mail_raw_open_stream(test_raw_mail_user, input_msg); ++ ++ edmail = edit_mail_wrap(rawmail->mail); ++ ++ /* Delete header */ ++ ++ edit_mail_header_delete(edmail, "X-B", 0); ++ ++ /* Add header */ ++ ++ edit_mail_header_add(edmail, "X-B", "Frop", TRUE); ++ mail = edit_mail_get_mail(edmail); ++ ++ /* Prepare tests */ ++ ++ if (mail_get_stream(mail, NULL, NULL, &input_mail) < 0) { ++ i_fatal("Failed to open mail stream: %s", ++ mailbox_get_last_error(mail->box, NULL)); ++ } ++ ++ buffer = buffer_create_dynamic(default_pool, 1024); ++ ++ /* Evaluate modified header */ ++ ++ test_assert(mail_get_first_header_utf8(mail, "X-B", &value) > 0 && ++ strcmp(value, "Frop") == 0); ++ ++ /* Added */ ++ ++ i_stream_seek(input_mail, 0); ++ buffer_set_used_size(buffer, 0); ++ ++ test_stream_data(input_mail, buffer); ++ ++ /* Clean up */ ++ ++ buffer_free(&buffer); ++ edit_mail_unwrap(&edmail); ++ mail_raw_close(&rawmail); ++ i_stream_unref(&input_msg); ++ test_edit_mail_deinit(); ++ test_end(); ++} ++ + int main(int argc, char *argv[]) + { + static void (*test_functions[])(void) = { +@@ -873,6 +1011,8 @@ int main(int argc, char *argv[]) + test_edit_mail_big_header, + test_edit_mail_small_buffer, + test_edit_mail_empty, ++ test_edit_mail_empty2, ++ test_edit_mail_nul_in_header, + NULL + }; + const enum master_service_flags service_flags = +Index: trixie/pigeonhole/tests/extensions/editheader/nul-value.svtest +=================================================================== +--- /dev/null ++++ trixie/pigeonhole/tests/extensions/editheader/nul-value.svtest +@@ -0,0 +1,46 @@ ++require "vnd.dovecot.testsuite"; ++require "encoded-character"; ++require "variables"; ++require "fileinto"; ++require "mailbox"; ++ ++require "editheader"; ++ ++/* ++ * Regression test: edit_mail_headers_parse() used i_strndup() to copy the ++ * raw header data, which stopped at the first NUL byte and allocated a buffer ++ * shorter than field->size. When the modified message was subsequently ++ * streamed (e.g. fileinto), memcpy() read field->size bytes out of that ++ * under-sized buffer, causing an invalid heap read detectable by valgrind. ++ */ ++ ++/* Construct a raw message that has a NUL byte inside a header value. ++ * ${hex:00} = NUL. field->size covers the full header line including ++ * bytes after the NUL; the old i_strndup() only allocated up to the NUL. */ ++test_set "message" text: ++From: sender@example.com ++X-Has-NUL: value${hex:00}afterNUL ++Subject: Test ++ ++Body ++. ++; ++ ++test "editheader - NUL byte in header value" { ++ /* deleteheader calls edit_mail_headers_parse(), which allocates ++ * field->data for every header including X-Has-NUL. With the old ++ * i_strndup() code, X-Has-NUL's allocation is only strlen("value")+1 ++ * bytes, but field->size covers the full extent including bytes after ++ * the NUL. deleteheader also sets modified=TRUE. */ ++ deleteheader "Subject"; ++ ++ fileinto :create "nul-test"; ++ ++ /* Streaming iterates over all remaining parsed headers including ++ * X-Has-NUL, reading field->size bytes from field->data. With the ++ * old under-sized allocation this is an invalid heap read that ++ * valgrind detects. */ ++ if not test_result_execute { ++ test_fail "failed to execute result"; ++ } ++} diff --git a/patches/0003-lib-smtp-smtp-server-Expose-smtp_server_connection_r.patch b/patches/0003-lib-smtp-smtp-server-Expose-smtp_server_connection_r.patch new file mode 100644 index 0000000..00c677a --- /dev/null +++ b/patches/0003-lib-smtp-smtp-server-Expose-smtp_server_connection_r.patch @@ -0,0 +1,51 @@ +From 12fcbfe65431ece3f49d39d553f22001b3b007b6 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 7 May 2026 10:57:59 +0000 +Subject: [PATCH 3/5] lib-smtp: smtp-server - Expose + smtp_server_connection_reply_immediate + +The function already existed internally as a way to write a reply directly +to the output stream, bypassing the per-command reply queue. Make it part +of the public API so callers that need a reply on the wire before tearing +the connection down (where queued replies would otherwise be aborted) can +use it. +--- + src/lib-smtp/smtp-server-private.h | 3 --- + src/lib-smtp/smtp-server.h | 10 ++++++++++ + 2 files changed, 10 insertions(+), 3 deletions(-) + +Index: trixie/src/lib-smtp/smtp-server-private.h +=================================================================== +--- trixie.orig/src/lib-smtp/smtp-server-private.h ++++ trixie/src/lib-smtp/smtp-server-private.h +@@ -323,9 +323,6 @@ void smtp_server_connection_reply_lines( + unsigned int status, + const char *enh_code, + const char *const *text_lines); +-void smtp_server_connection_reply_immediate( +- struct smtp_server_connection *conn, unsigned int status, +- const char *fmt, ...) ATTR_FORMAT(3, 4); + + void smtp_server_connection_reset_state(struct smtp_server_connection *conn); + void smtp_server_connection_set_state(struct smtp_server_connection *conn, +Index: trixie/src/lib-smtp/smtp-server.h +=================================================================== +--- trixie.orig/src/lib-smtp/smtp-server.h ++++ trixie/src/lib-smtp/smtp-server.h +@@ -477,6 +477,16 @@ void smtp_server_connection_set_ssl_stre + void smtp_server_connection_close(struct smtp_server_connection **_conn, + const char *reason) ATTR_NULL(2); + bool smtp_server_connection_is_closed(struct smtp_server_connection *conn); ++/* Send a reply line directly to the output stream, bypassing the per-command ++ reply queue. Useful when the caller is about to close the connection and ++ needs the reply on the wire before close/destroy aborts queued replies. ++ The formatted text must be a single line (no CR/LF); unlike ++ smtp_server_reply_add_text(), this function does not split on \n nor add ++ the SMTP continuation prefix to subsequent lines. */ ++void smtp_server_connection_reply_immediate(struct smtp_server_connection *conn, ++ unsigned int status, ++ const char *fmt, ...) ++ ATTR_FORMAT(3, 4); + void smtp_server_connection_terminate(struct smtp_server_connection **_conn, + const char *enh_code, const char *reason) + ATTR_NULL(3); diff --git a/patches/0003-lib-sql-Add-sql_commit_schedule_delayed-helper.patch b/patches/0003-lib-sql-Add-sql_commit_schedule_delayed-helper.patch new file mode 100644 index 0000000..e602549 --- /dev/null +++ b/patches/0003-lib-sql-Add-sql_commit_schedule_delayed-helper.patch @@ -0,0 +1,50 @@ +From 3c388ec11db1d61a2795241321d80a7dc245105c Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Tue, 2 Jun 2026 09:54:41 +0000 +Subject: [PATCH 3/6] lib-sql: Add sql_commit_schedule_delayed() helper + +--- + src/lib-sql/sql-api.c | 21 ++++++++++++++------- + 1 file changed, 14 insertions(+), 7 deletions(-) + +Index: trixie/src/lib-sql/sql-api.c +=================================================================== +--- trixie.orig/src/lib-sql/sql-api.c ++++ trixie/src/lib-sql/sql-api.c +@@ -885,6 +885,19 @@ static void sql_commit_delayed_callback( + i_free(cb); + } + ++static void ++sql_commit_schedule_delayed(struct sql_db *db, const char *error, ++ sql_commit_callback_t *callback, void *context) ++{ ++ struct sql_commit_result_delayed *cb = i_new(struct sql_commit_result_delayed, 1); ++ cb->db = db; ++ cb->error = i_strdup(error); ++ cb->callback = callback; ++ cb->context = context; ++ cb->to = timeout_add_short(0, sql_commit_delayed_callback, cb); ++ DLLIST_PREPEND(&db->commit_delayed_list, cb); ++} ++ + #undef sql_transaction_commit + void sql_transaction_commit(struct sql_transaction_context **_ctx, + sql_commit_callback_t *callback, void *context) +@@ -898,15 +911,9 @@ void sql_transaction_commit(struct sql_t + return; + } + +- struct sql_commit_result_delayed *cb = i_new(struct sql_commit_result_delayed, 1); + const char *error = NULL; + ctx->db->v.transaction_commit_s(ctx, &error); +- cb->db = db; +- cb->error = i_strdup(error); +- cb->callback = callback; +- cb->context = context; +- cb->to = timeout_add_short(0, sql_commit_delayed_callback, cb); +- DLLIST_PREPEND(&db->commit_delayed_list, cb); ++ sql_commit_schedule_delayed(db, error, callback, context); + } + + int sql_transaction_commit_s(struct sql_transaction_context **_ctx, diff --git a/patches/0003-lib-storage-thread-Limit-ancestor-chain-traversal-de.patch b/patches/0003-lib-storage-thread-Limit-ancestor-chain-traversal-de.patch new file mode 100644 index 0000000..4771e5e --- /dev/null +++ b/patches/0003-lib-storage-thread-Limit-ancestor-chain-traversal-de.patch @@ -0,0 +1,92 @@ +From e6934c6d1afdcecd63591d7f514ccb39e11d6ada Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 17 Apr 2026 13:15:54 +0000 +Subject: [PATCH 3/3] lib-storage: thread - Limit ancestor chain traversal + depth to prevent O(N^2) CPU usage + +The per-message References limit (MAIL_THREAD_REFERENCES_MAX) prevents a +single crafted message from causing O(N^2) traversals in +thread_node_has_ancestor(). However, multiple crafted messages each +containing 1000 References entries can build an arbitrarily deep ancestor +chain across the mailbox, causing the same quadratic blowup spread over +many messages: processing email k costs O(k * MAIL_THREAD_REFERENCES_MAX) +steps, giving O(M^2 * MAIL_THREAD_REFERENCES_MAX) total for M emails. + +Fix this by limiting the traversal depth in thread_node_has_ancestor() to +MAIL_THREAD_REFERENCES_MAX steps. When the limit is reached the link is +dropped, bounding per-email work to O(MAIL_THREAD_REFERENCES_MAX^2) +regardless of how deep the chain was built by prior messages. +--- + src/lib-storage/index/index-thread-links.c | 31 +++++++++++++++++++--- + 1 file changed, 28 insertions(+), 3 deletions(-) + +diff --git a/src/lib-storage/index/index-thread-links.c b/src/lib-storage/index/index-thread-links.c +index 9affb8338d..0ce33a0772 100644 +--- a/src/lib-storage/index/index-thread-links.c ++++ b/src/lib-storage/index/index-thread-links.c +@@ -31,12 +31,24 @@ static uint32_t thread_msg_add(struct mail_thread_cache *cache, + + static bool thread_node_has_ancestor(struct mail_thread_cache *cache, + const struct mail_thread_node *node, +- const struct mail_thread_node *ancestor) ++ const struct mail_thread_node *ancestor, ++ bool *depth_exceeded_r) + { ++ unsigned int n = 0; ++ ++ *depth_exceeded_r = FALSE; + while (node != ancestor) { + if (node->parent_idx == 0) + return FALSE; +- ++ if (++n > MAIL_THREAD_REFERENCES_MAX) { ++ /* Ancestor chain is deeper than the per-message ++ References limit. Multiple crafted messages can build ++ an arbitrarily deep chain, causing O(N^2) traversals ++ per email even with the per-message limit. Treat the ++ link as unsafe to prevent the DoS. */ ++ *depth_exceeded_r = TRUE; ++ return FALSE; ++ } + node = array_idx(&cache->thread_nodes, node->parent_idx); + } + return TRUE; +@@ -47,6 +59,7 @@ static void thread_link_reference(struct mail_thread_cache *cache, + { + struct mail_thread_node *node, *parent, *child; + uint32_t idx; ++ bool depth_exceeded; + + i_assert(parent_idx < cache->first_invalid_msgid_str_idx); + +@@ -62,7 +75,7 @@ static void thread_link_reference(struct mail_thread_cache *cache, + } + + child->parent_link_refcount++; +- if (thread_node_has_ancestor(cache, parent, child)) { ++ if (thread_node_has_ancestor(cache, parent, child, &depth_exceeded)) { + if (parent == child) { + /* loops to itself - ignore */ + return; +@@ -88,6 +101,18 @@ static void thread_link_reference(struct mail_thread_cache *cache, + node->child_unref_rebuilds = TRUE; + } while (node != child); + return; ++ } else if (depth_exceeded) { ++ /* Ancestor chain exceeded the depth limit; drop this link. ++ parent_link_refcount was already incremented above and is ++ intentionally kept that way: mail_thread_unref_link() ++ walks References blindly at remove time and would hit ++ i_assert(child->parent_link_refcount > 0) on the matching ++ edge if we hadn't bumped it here. The trade-off is that ++ the bumped refcount can leave child->parent_idx attached ++ to a stale parent if another message later sets a real ++ parent edge on this child and is then expunged - threading ++ stays wrong until some other event triggers a rebuild. */ ++ return; + } else if (child->parent_idx == parent_idx) { + /* The same link already exists */ + return; +-- +2.47.3 + diff --git a/patches/0004-acl-dovecot-acl-list-is-never-supposed-to-be-a-symli.patch b/patches/0004-acl-dovecot-acl-list-is-never-supposed-to-be-a-symli.patch new file mode 100644 index 0000000..382bfbd --- /dev/null +++ b/patches/0004-acl-dovecot-acl-list-is-never-supposed-to-be-a-symli.patch @@ -0,0 +1,26 @@ +From a5d06e714ed25a6cb18fd3af99314e94fa080046 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 3 Jun 2026 18:16:38 +0300 +Subject: [PATCH 4/5] acl: dovecot-acl-list is never supposed to be a symlink - + use O_NOFOLLOW flag + +--- + src/plugins/acl/acl-backend-vfile-acllist.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/plugins/acl/acl-backend-vfile-acllist.c b/src/plugins/acl/acl-backend-vfile-acllist.c +index 7dfcfdd393..d291b55594 100644 +--- a/src/plugins/acl/acl-backend-vfile-acllist.c ++++ b/src/plugins/acl/acl-backend-vfile-acllist.c +@@ -115,7 +115,7 @@ static int acl_backend_vfile_acllist_read(struct acl_backend_vfile *backend) + return 0; + } + +- fd = open(path, O_RDONLY); ++ fd = open(path, O_RDONLY | O_NOFOLLOW); + if (fd == -1) { + if (errno == ENOENT) { + backend->acllist_mtime = 0; +-- +2.39.5 + diff --git a/patches/0004-imap-login-Add-dict_reset-side-channel-command-to-im.patch b/patches/0004-imap-login-Add-dict_reset-side-channel-command-to-im.patch new file mode 100644 index 0000000..f30699d --- /dev/null +++ b/patches/0004-imap-login-Add-dict_reset-side-channel-command-to-im.patch @@ -0,0 +1,55 @@ +From da4c52ac021b32bfc9f926f3c229c91ecb729cca Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 16 Apr 2026 17:38:45 +0200 +Subject: [PATCH 4/5] imap-login: Add dict_reset side-channel command to + imap-proxy + +When imap_compress_on_proxy is enabled, DEFLATE compression runs inside +the imap-login process. The imap backend process cannot call +o_stream_deflate_reset_dict() directly on a remote stream, so this adds +a new side-channel command "dict_reset" that the backend can send to +trigger the dictionary reset on the proxy side. +--- + src/imap-login/imap-proxy.c | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +diff --git a/src/imap-login/imap-proxy.c b/src/imap-login/imap-proxy.c +index 03dad690e1..2084cb290d 100644 +--- a/src/imap-login/imap-proxy.c ++++ b/src/imap-login/imap-proxy.c +@@ -10,6 +10,7 @@ + #include "str-sanitize.h" + #include "safe-memset.h" + #include "compression.h" ++#include "ostream-zlib.h" + #include "dsasl-client.h" + #include "imap-login-client.h" + #include "client-authenticate.h" +@@ -641,11 +642,24 @@ proxy_side_cmd_compress(struct client *client, const char *const *args, + return 0; + } + ++static int ++proxy_side_cmd_dict_reset(struct client *client, ++ const char *const *args ATTR_UNUSED, ++ const char **error_r ATTR_UNUSED) ++{ ++ struct ostream *client_output = ++ login_proxy_get_client_ostream(client->login_proxy); ++ o_stream_deflate_reset_dict(client_output); ++ return 0; ++} ++ + int imap_proxy_side_channel_input(struct client *client, + const char *const *args, const char **error_r) + { + if (strcmp(args[0], "compress") == 0) + return proxy_side_cmd_compress(client, args + 1, error_r); ++ else if (strcmp(args[0], "dict_reset") == 0) ++ return proxy_side_cmd_dict_reset(client, args + 1, error_r); + else { + *error_r = "Unsupported command"; + return -1; +-- +2.47.3 + diff --git a/patches/0004-lib-Key-str_hash-strcase_hash-with-a-random-seed-to-.patch b/patches/0004-lib-Key-str_hash-strcase_hash-with-a-random-seed-to-.patch new file mode 100644 index 0000000..059898c --- /dev/null +++ b/patches/0004-lib-Key-str_hash-strcase_hash-with-a-random-seed-to-.patch @@ -0,0 +1,120 @@ +From b9afacaf42e7b2efff769dbc3eb10ac48934462a Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 13 Apr 2026 12:38:50 +0200 +Subject: [PATCH 04/12] lib: Key str_hash/strcase_hash with a random seed to + prevent HashDoS + +hash_init() fills a process-wide uint64_t hash_iv via random_fill() +at lib_init() time (after random_init()). str_hash() and strcase_hash() +pass hash_iv as the xxh64 seed so an attacker cannot predict bucket +placement and manufacture collision chains. +--- + src/lib/hash.c | 12 ++++++++++-- + src/lib/hash.h | 10 +++++++++- + src/lib/lib.c | 2 ++ + 3 files changed, 21 insertions(+), 3 deletions(-) + +diff --git a/src/lib/hash.c b/src/lib/hash.c +index 61fe86ed8e..83eeed42f6 100644 +--- a/src/lib/hash.c ++++ b/src/lib/hash.c +@@ -5,6 +5,7 @@ + #include "lib.h" + #include "hash.h" + #include "primes.h" ++#include "randgen.h" + #include "xxh64.h" + + #include +@@ -59,6 +60,8 @@ enum hash_table_operation{ + HASH_TABLE_OP_RESIZE + }; + ++uint64_t hash_iv; ++ + static bool hash_table_resize(struct hash_table *table, bool grow); + + void hash_table_create(struct hash_table **table_r, pool_t node_pool, +@@ -520,9 +523,14 @@ void hash_table_copy(struct hash_table *dest, struct hash_table *src) + hash_table_thaw(dest); + } + ++void hash_init(void) ++{ ++ random_fill(&hash_iv, sizeof(hash_iv)); ++} ++ + unsigned int str_hash(const char *p) + { +- return xxh64_to_32(xxh64_data(p, strlen(p), 0)); ++ return xxh64_to_32(xxh64_data(p, strlen(p), hash_iv)); + } + + unsigned int str_stable_hash(const char *p) +@@ -535,7 +543,7 @@ unsigned int strcase_hash(const char *p) + struct xxh64_context ctx; + unsigned char c; + +- xxh64_init(&ctx, 0); ++ xxh64_init(&ctx, hash_iv); + while (*p != '\0') { + c = (unsigned char)i_toupper(*p++); + xxh64_loop(&ctx, &c, 1); +diff --git a/src/lib/hash.h b/src/lib/hash.h +index 84d9c52aef..534c25f6ee 100644 +--- a/src/lib/hash.h ++++ b/src/lib/hash.h +@@ -14,6 +14,9 @@ typedef unsigned int hash_callback_t(const void *p); + /* Returns 0 if the pointers are equal. */ + typedef int hash_cmp_callback_t(const void *p1, const void *p2); + ++/* Random per-process IV to use for hash functions */ ++extern uint64_t hash_iv; ++ + /* Create a new hash table. If initial_size is 0, the default value is used. + table_pool is used to allocate/free large hash tables, node_pool is used + for smaller allocations and can also be alloconly pool. The pools must not +@@ -168,9 +171,12 @@ void hash_table_copy(struct hash_table *dest, struct hash_table *src); + #define hash_table_copy(table1, table2) \ + hash_table_copy((table1)._table, (table2)._table) + +-/* hash function for strings */ ++/* Hash function for strings. These are safe against collision attacks. They ++ are initialized with a per-process random key. */ + unsigned int str_hash(const char *p) ATTR_PURE; + unsigned int strcase_hash(const char *p) ATTR_PURE; ++/* Like str_hash(), but there is no per-process random key. This isn't as safe ++ against collision attacks. */ + unsigned int str_stable_hash(const char *p) ATTR_PURE; + + /* fast hash function which uppercases a-z. Does not work well +@@ -181,4 +187,6 @@ unsigned int strfastcase_hash(const char *p) ATTR_PURE; + /* a generic hash for a given memory block */ + unsigned int mem_hash(const void *p, unsigned int size) ATTR_PURE; + ++void hash_init(void); ++ + #endif +diff --git a/src/lib/lib.c b/src/lib/lib.c +index 4df939b7a1..4a4372f516 100644 +--- a/src/lib/lib.c ++++ b/src/lib/lib.c +@@ -5,6 +5,7 @@ + #include "array.h" + #include "event-filter.h" + #include "env-util.h" ++#include "hash.h" + #include "hostpid.h" + #include "ipwd.h" + #include "process-title.h" +@@ -184,6 +185,7 @@ void lib_init(void) + { + i_assert(!lib_initialized); + random_init(); ++ hash_init(); + data_stack_init(); + hostpid_init(); + lib_open_non_stdio_dev_null(); +-- +2.47.3 + diff --git a/patches/0004-lib-compression-test-Add-zero-len-frame-test-for-all.patch b/patches/0004-lib-compression-test-Add-zero-len-frame-test-for-all.patch new file mode 100644 index 0000000..e432e08 --- /dev/null +++ b/patches/0004-lib-compression-test-Add-zero-len-frame-test-for-all.patch @@ -0,0 +1,72 @@ +From 32ef85eff6329395b74e47ec69bf7926c629d7c6 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Fri, 12 Jun 2026 11:19:42 +0000 +Subject: [PATCH 4/4] lib-compression: test - Add zero-len frame test for all + handlers + +Verify that successive empty compressed frames produce a clean EOF +across all compression algorithms. lz4 is limited to one frame as it +uses a custom single-stream format without concatenation support. +--- + src/lib-compression/test-compression.c | 41 ++++++++++++++++++++++++++ + 1 file changed, 41 insertions(+) + +Index: trixie/src/lib-compression/test-compression.c +=================================================================== +--- trixie.orig/src/lib-compression/test-compression.c ++++ trixie/src/lib-compression/test-compression.c +@@ -155,6 +155,46 @@ test_compression_handler_empty(const str + } + + static void ++test_compression_handler_zero_frame(const struct compression_handler *handler, ++ bool autodetect) ++{ ++ test_begin(t_strdup_printf("compression handler %s (zero-len frame, autodetect=%s)", ++ handler->name, autodetect ? "yes" : "no")); ++ ++ /* Produce successive empty frames. Each frame needs its own ostream ++ because o_stream_finish() propagates to the parent; compress each ++ into a separate buffer then concatenate. ++ lz4 uses a custom single-stream format and does not support ++ concatenated frames, so limit it to one. */ ++ unsigned int n_frames = strcmp(handler->name, "lz4") == 0 ? 1 : 3; ++ buffer_t *compressed = buffer_create_dynamic(pool_datastack_create(), 256); ++ for (unsigned int i = 0; i < n_frames; i++) { ++ buffer_t *frame_buf = buffer_create_dynamic(pool_datastack_create(), 64); ++ struct ostream *os = test_ostream_create(frame_buf); ++ struct ostream *output = handler->create_ostream_auto(os, set.event); ++ o_stream_unref(&os); ++ test_assert_idx(o_stream_finish(output) == 1, i); ++ o_stream_unref(&output); ++ test_assert_idx(frame_buf->used > 0, i); ++ buffer_append(compressed, frame_buf->data, frame_buf->used); ++ } ++ ++ /* Decompress: must yield clean EOF with no error and no loop. */ ++ struct istream *is = test_istream_create_data(compressed->data, compressed->used); ++ is->blocking = TRUE; ++ struct istream *input = !autodetect ? handler->create_istream(is) : ++ i_stream_create_decompress(is, 0); ++ i_stream_unref(&is); ++ ++ test_assert(i_stream_read(input) == -1); ++ test_assert(input->eof); ++ test_assert(input->stream_errno == 0); ++ i_stream_unref(&input); ++ ++ test_end(); ++} ++ ++static void + test_compression_handler_seek(const struct compression_handler *handler, + bool autodetect) + { +@@ -743,6 +783,7 @@ static void test_compression_int(bool au + test_compression_handler_detect(&compression_handlers[i]); + test_compression_handler_short(&compression_handlers[i], autodetect); + test_compression_handler_empty(&compression_handlers[i], autodetect); ++ test_compression_handler_zero_frame(&compression_handlers[i], autodetect); + test_compression_handler(&compression_handlers[i], autodetect); + test_compression_handler_seek(&compression_handlers[i], autodetect); + test_compression_handler_reset(&compression_handlers[i], autodetect); diff --git a/patches/0004-lib-mail-message-parser-from-parts-Enforce-50-MB-all.patch b/patches/0004-lib-mail-message-parser-from-parts-Enforce-50-MB-all.patch new file mode 100644 index 0000000..31de9be --- /dev/null +++ b/patches/0004-lib-mail-message-parser-from-parts-Enforce-50-MB-all.patch @@ -0,0 +1,52 @@ +From afb5fd924bf39dc790a573151e2d2626a63d0aec Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 16 Apr 2026 22:38:02 +0200 +Subject: [PATCH 04/14] lib-mail: message-parser-from-parts: Enforce 50 MB + all-headers-max-size limit + +The preparsed (from-parts) header parser was not tracking +all_headers_total_size and never called message_parse_header_lower_limit(), +so the cumulative 50 MB header size limit was never applied when re-parsing +a message using cached part structure. + +Fix by mirroring the same tracking that message-parser.c does in its +parse_next_header_block(): update all_headers_total_size for each parsed +header line, and call message_parse_header_lower_limit() with the remaining +budget when initialising the per-part header parser. + +Co-Authored-By: Claude Sonnet 4.6 +--- + src/lib-mail/message-parser-from-parts.c | 10 ++++++++++ + 1 file changed, 10 insertions(+) + +diff --git a/src/lib-mail/message-parser-from-parts.c b/src/lib-mail/message-parser-from-parts.c +index 867a916c1c..fb75dc7148 100644 +--- a/src/lib-mail/message-parser-from-parts.c ++++ b/src/lib-mail/message-parser-from-parts.c +@@ -303,6 +303,11 @@ static int preparsed_parse_next_header(struct message_parser_ctx *ctx, + } + + if (hdr != NULL) { ++ if (!hdr->continues) { ++ ctx->all_headers_total_size += hdr->name_len; ++ ctx->all_headers_total_size += hdr->middle_len; ++ } ++ ctx->all_headers_total_size += hdr->value_len; + block_r->hdr = hdr; + block_r->size = 0; + return 1; +@@ -344,6 +349,11 @@ static int preparsed_parse_next_header_init(struct message_parser_ctx *ctx, + message_parse_header_init(hdr_input, NULL, ctx->hdr_flags); + i_stream_unref(&hdr_input); + ++ size_t headers_available = ++ ctx->all_headers_max_size > ctx->all_headers_total_size ? ++ ctx->all_headers_max_size - ctx->all_headers_total_size : 0; ++ message_parse_header_lower_limit(ctx->hdr_parser_ctx, headers_available); ++ + ctx->parse_next_block = preparsed_parse_next_header; + return preparsed_parse_next_header(ctx, block_r); + } +-- +2.47.3 + diff --git a/patches/0004-lib-mail-o_stream_dot_sendv-Do-not-send-unguarded-.-.patch b/patches/0004-lib-mail-o_stream_dot_sendv-Do-not-send-unguarded-.-.patch new file mode 100644 index 0000000..84b2306 --- /dev/null +++ b/patches/0004-lib-mail-o_stream_dot_sendv-Do-not-send-unguarded-.-.patch @@ -0,0 +1,216 @@ +From dc1f33030101d90fcb77612eb3a111854525506a Mon Sep 17 00:00:00 2001 +From: Marco Bettini +Date: Wed, 8 Apr 2026 10:14:08 +0000 +Subject: [PATCH 4/4] lib-mail: o_stream_dot_sendv() - Do not send unguarded + '.' after bare '\r' + +--- + src/lib-mail/ostream-dot.c | 40 ++++++++++++++++-- + src/lib-mail/test-iostream-dot.c | 9 ++++ + src/lib-mail/test-ostream-dot.c | 71 ++++++++++++++++++++++++++++++-- + 3 files changed, 112 insertions(+), 8 deletions(-) + +Index: trixie/src/lib-mail/ostream-dot.c +=================================================================== +--- trixie.orig/src/lib-mail/ostream-dot.c ++++ trixie/src/lib-mail/ostream-dot.c +@@ -71,10 +71,12 @@ o_stream_dot_close(struct iostream_priva + o_stream_close(dstream->ostream.parent); + } + ++#define ADD_MAX 3 + enum o_stream_dot_sendv_add { + ADD_NONE, + ADD_CR, + ADD_DOT, ++ ADD_LF_DOT, + }; + + static ssize_t +@@ -111,14 +113,14 @@ o_stream_dot_sendv(struct ostream_privat + + p = data; + pend = CONST_PTR_OFFSET(data, size); +- for (; p < pend && ((size_t)(p - data) + 2) <= max_bytes; p++) { ++ for (; p < pend && ((size_t)(p - data) + ADD_MAX) <= max_bytes; p++) { + enum o_stream_dot_sendv_add add = ADD_NONE; + + size = pend - p; + switch (dstream->state) { + /* none */ + case STREAM_STATE_NONE: { +- size_t maxlen = I_MIN(size, max_bytes - ((size_t)(p - data) + 2)); ++ size_t maxlen = I_MIN(size, max_bytes - ((size_t)(p - data) + ADD_MAX)); + p = CONST_PTR_OFFSET(p, i_memcspn(p, maxlen, "\r\n", 2)); + i_assert(p <= pend); + if (p == pend) { +@@ -145,6 +147,9 @@ o_stream_dot_sendv(struct ostream_privat + case '\n': + dstream->state = STREAM_STATE_CRLF; + break; ++ case '.': ++ add = ADD_LF_DOT; ++ /* fall through */ + default: + dstream->state = STREAM_STATE_NONE; + break; +@@ -186,9 +191,31 @@ o_stream_dot_sendv(struct ostream_privat + max_bytes -= chunk; + sent += chunk; + } +- /* insert byte (substitute one with pair) */ + data++; + ++ /* Apply the modifications required according to RFC 5321 to the ++ stream : ++ ++ ADD_DOT - Apply standard SMTP dot-stuffing ++ ++ ADD_CR - Convert invalid lone LFs to CRLF to comply with ++ DATA line termination requirements, and avoid ++ potentially emitting bare "\n." sequences (same ++ threat as below for "\r." sequences). ++ ++ ADD_LF_DOT - Guard against invalid "\r." sequences. ++ A downstream MTA that incorrectly handles bare CRs ++ as line terminators could interpret a '.' at the ++ start of the next line as end-of-DATA. This would ++ allow any following bytes to be processed as SMTP ++ commands. ++ ++ ADD_CR and ADD_LF_DOT make the istream-dot round-trip lossy, ++ as these introduce bytes not present in the original input. ++ We accept this tradeoff, since preserving exact byte fidelity ++ for malformed input is less important than eliminating an ++ injection vector. */ ++ + switch(add) { + case ADD_DOT: + iovn.iov_base = ".."; +@@ -198,12 +225,17 @@ o_stream_dot_sendv(struct ostream_privat + iovn.iov_base = "\r\n"; + iovn.iov_len = 2; + break; ++ case ADD_LF_DOT: ++ iovn.iov_base = "\n.."; ++ iovn.iov_len = 3; ++ break; + default: + i_unreached(); + } + + array_push_back(&iov_arr, &iovn); +- i_assert(max_bytes >= iovn.iov_len); ++ i_assert(iovn.iov_len <= ADD_MAX); ++ i_assert(iovn.iov_len <= max_bytes); + max_bytes -= iovn.iov_len; + added += iovn.iov_len - 1; + sent++; +Index: trixie/src/lib-mail/test-ostream-dot.c +=================================================================== +--- trixie.orig/src/lib-mail/test-ostream-dot.c ++++ trixie/src/lib-mail/test-ostream-dot.c +@@ -57,6 +57,9 @@ static void test_ostream_dot(void) + { "foo\n.\n", "foo\r\n..\r\n.\r\n" }, + { ".foo\r\n.\r\nfoo\r\n", "..foo\r\n..\r\nfoo\r\n.\r\n" }, + { ".foo\n.\nfoo\n", "..foo\r\n..\r\nfoo\r\n.\r\n" }, ++ { ".", "..\r\n.\r\n" }, ++ { "\r.", "\r\n..\r\n.\r\n" }, ++ { "\r\r.", "\r\r\n..\r\n.\r\n" }, + { "\r\n", "\r\n.\r\n" }, + { "\n", "\r\n.\r\n" }, + { "", "\r\n.\r\n" }, +@@ -92,7 +95,7 @@ static void test_ostream_dot_parent_almo + test_end(); + } + +-static void test_ostream_dot_parent_exact_fit(void) ++static void test_ostream_dot_parent_max_bytes_boundary(void) + { + buffer_t *output_data; + struct ostream *test_output, *output; +@@ -100,8 +103,8 @@ static void test_ostream_dot_parent_exac + + test_begin("dot ostream parent exact fit"); + output_data = t_buffer_create(1024); +- test_output = test_ostream_create_nonblocking(output_data, 2); +- test_ostream_set_max_output_size(test_output, 2); ++ test_output = test_ostream_create_nonblocking(output_data, 3); ++ test_ostream_set_max_output_size(test_output, 3); + + output = o_stream_create_dot(test_output, FALSE); + ret = o_stream_send(output, ".", 1); +@@ -114,12 +117,72 @@ static void test_ostream_dot_parent_exac + test_end(); + } + ++/* STATE_CR must persist across sendv calls so that a bare CR ending one send ++ followed by '.' starting the next still triggers ADD_LF_DOT. ++ A stateless per-call implementation would emit the '.' unguarded and let a ++ downstream MTA that treats bare CR as EOL interpret it as end-of-DATA. */ ++static void test_ostream_dot_cr_across_sendv(void) ++{ ++ buffer_t *output_data; ++ struct ostream *test_output, *output; ++ ssize_t ret; ++ const char *expected = "\r\n..\r\n.\r\n"; ++ ++ test_begin("dot ostream CR across sendv calls"); ++ output_data = t_buffer_create(1024); ++ test_output = o_stream_create_buffer(output_data); ++ ++ output = o_stream_create_dot(test_output, FALSE); ++ ret = o_stream_send(output, "\r", 1); ++ test_assert(ret == 1); ++ ret = o_stream_send(output, ".", 1); ++ test_assert(ret == 1); ++ test_assert(o_stream_finish(output) > 0); ++ ++ o_stream_unref(&output); ++ o_stream_unref(&test_output); ++ ++ test_assert_ucmp(str_len(output_data), ==, strlen(expected)); ++ test_assert_memcmp(str_c(output_data), str_len(output_data), ++ expected, strlen(expected)); ++ test_end(); ++} ++ ++/* Off-by-one regression guard for the bare-CR + '.' injection. ++ Input "\r." places '.' at offset 1; the ADD_LF_DOT pair injects 3 bytes ++ ("\n.."). The loop guard check becomes (1 + 3) <= max_bytes, so max_bytes==4 ++ is the exact-fit case. */ ++static void test_ostream_dot_cr_dot_exact_fit(void) ++{ ++ buffer_t *output_data; ++ struct ostream *test_output, *output; ++ ssize_t ret; ++ ++ test_begin("dot ostream CR+dot exact fit"); ++ output_data = t_buffer_create(1024); ++ test_output = test_ostream_create_nonblocking(output_data, 4); ++ test_ostream_set_max_output_size(test_output, 4); ++ ++ output = o_stream_create_dot(test_output, FALSE); ++ ret = o_stream_send(output, "\r.", 2); ++ test_assert(ret == 2); ++ test_assert_ucmp(output_data->used, ==, 4); ++ test_assert_memcmp(output_data->data, output_data->used, ++ "\r\n..", 4); ++ ++ o_stream_unref(&output); ++ o_stream_unref(&test_output); ++ test_end(); ++} ++ + int main(void) + { + static void (*const test_functions[])(void) = { + test_ostream_dot, + test_ostream_dot_parent_almost_full, +- test_ostream_dot_parent_exact_fit, ++ test_ostream_dot_parent_max_bytes_boundary, ++ test_ostream_dot_cr_across_sendv, ++ test_ostream_dot_cr_dot_exact_fit, + NULL + }; + return test_run(test_functions); diff --git a/patches/0004-lib-sieve-Switch-sieve_max_cpu_time-enforcement-to-t.patch b/patches/0004-lib-sieve-Switch-sieve_max_cpu_time-enforcement-to-t.patch new file mode 100644 index 0000000..c6c7f99 --- /dev/null +++ b/patches/0004-lib-sieve-Switch-sieve_max_cpu_time-enforcement-to-t.patch @@ -0,0 +1,294 @@ +From 7a55953924c54dc116150ae809b976456977db75 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 5 May 2026 16:30:23 +0000 +Subject: [PATCH 4/7] lib-sieve: Switch sieve_max_cpu_time enforcement to + time-decayed re-enable + +When a user's cumulative CPU time exceeded sieve_max_cpu_time, the +script was permanently disabled. SIEVE_BINARY_FLAG_RESOURCE_LIMIT was +set in the binary header on first overrun and was never cleared by +the resource_usage_timeout decay (which only zeroed the cpu_time +field), so sieve_binary_check_executable() refused execution forever +unless the binary was recompiled or an admin manually wiped the file. + +Replace the sticky-flag model with one driven purely by the persisted +cumulative cpu_time, decayed by resource_usage_timeout: + + - Drop SIEVE_BINARY_FLAG_RESOURCE_LIMIT and the enum that defined it; + nothing reads sbin->header.flags anymore. + - sieve_binary_check_executable() and sieve_binary_check_resource_usage() + call sieve_resource_usage_is_excessive() on the current cumulative + cpu_time. Raising sieve_max_cpu_time now correctly re-enables a + previously over-limit user without further intervention. + - Gate per-user file persistence on the per-run delta, not the + cumulative total. A refused-without-running attempt has a zero + delta, skips the file write, and leaves update_time at the moment + work was actually consumed. After resource_usage_timeout seconds + of inactivity (or only-trivial deliveries) the next load decays + the cpu_time to zero and the user is allowed to run again. + +The on-disk per-user sieve-rusage file format becomes: + + V1 +--- + src/lib-sieve/sieve-binary-file.c | 28 ++++++++++++++-------------- + src/lib-sieve/sieve-binary-private.h | 4 ---- + src/lib-sieve/sieve-binary.c | 13 ++----------- + src/lib-sieve/sieve-rusage.c | 25 ++++++++----------------- + src/lib-sieve/sieve-rusage.h | 14 +++++--------- + 5 files changed, 29 insertions(+), 55 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-file.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c +@@ -315,7 +315,7 @@ sieve_binary_save_to_stream(struct sieve + header->blocks = blk_count; + header->hdr_size = sizeof(*header); + +- header->flags &= ENUM_NEGATE(SIEVE_BINARY_FLAG_RESOURCE_LIMIT); ++ header->flags = 0; + sieve_binary_file_zero_header_rusage(sbin); + + if (!_save_aligned(sbin, stream, header, sizeof(*header), NULL)) { +@@ -837,7 +837,7 @@ _sieve_binary_open(struct sieve_binary * + discard any data from older versions. */ + sbin->header.unused_update_time = 0; + sbin->header.unused_cpu_time_msecs = 0; +- sbin->header.flags &= ENUM_NEGATE(SIEVE_BINARY_FLAG_RESOURCE_LIMIT); ++ sbin->header.flags = 0; + + /* Load block index */ + +@@ -934,15 +934,16 @@ int sieve_binary_check_executable(struct + enum sieve_error *error_code_r, + const char **client_error_r) + { ++ struct sieve_resource_usage rusage; ++ + *client_error_r = NULL; + sieve_error_args_init(&error_code_r, NULL); + +- if (HAS_ALL_BITS(sbin->header.flags, +- SIEVE_BINARY_FLAG_RESOURCE_LIMIT)) { ++ sieve_binary_get_resource_usage(sbin, &rusage); ++ if (sieve_resource_usage_is_excessive(sbin->svinst, &rusage)) { + e_debug(sbin->event, + "Binary execution is blocked: " +- "Cumulative resource usage limit exceeded " +- "(resource limit flag is set)"); ++ "Cumulative resource usage limit exceeded"); + *error_code_r = SIEVE_ERROR_RESOURCE_LIMIT; + *client_error_r = "cumulative resource usage limit exceeded"; + return 0; +@@ -959,8 +960,6 @@ int sieve_binary_file_update_resource_us + { + struct sieve_storage *storage; + struct sieve_resource_usage delta; +- struct sieve_resource_usage total; +- uint32_t flags; + + sieve_error_args_init(&error_code_r, NULL); + +@@ -976,18 +975,19 @@ int sieve_binary_file_update_resource_us + storage = sbin->script->storage; + + delta = sbin->rusage; +- sieve_binary_get_resource_usage(sbin, &total); +- flags = sbin->header.flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT; + +- if (!HAS_ALL_BITS(flags, SIEVE_BINARY_FLAG_RESOURCE_LIMIT) && +- !sieve_resource_usage_is_high(sbin->svinst, &total)) { +- /* Nothing meaningful to persist */ ++ /* Persist only when this run consumed a meaningful amount of CPU. ++ Critically, refused-without-running attempts (delta == 0) must not ++ touch the file - otherwise the file's update_time would be refreshed ++ on every blocked attempt, preventing the resource_usage_timeout from ++ ever decaying the persisted CPU time and re-enabling the user. */ ++ if (!sieve_resource_usage_is_high(sbin->svinst, &delta)) { + sieve_resource_usage_init(&sbin->rusage); + sbin->rusage_updated = FALSE; + return 0; + } + +- if (sieve_rusage_storage_add(storage, &delta, flags) < 0) { ++ if (sieve_rusage_storage_add(storage, &delta) < 0) { + *error_code_r = SIEVE_ERROR_TEMP_FAILURE; + return -1; + } +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-private.h ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h +@@ -13,10 +13,6 @@ + * Binary file + */ + +-enum SIEVE_BINARY_FLAGS { +- SIEVE_BINARY_FLAG_RESOURCE_LIMIT = BIT(0), +-}; +- + struct sieve_binary_header { + uint32_t magic; + uint16_t version_major; +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary.c +@@ -210,17 +210,14 @@ void sieve_binary_apply_persisted_rusage + { + struct sieve_storage *storage = sieve_binary_get_user_storage(sbin); + struct sieve_resource_usage rusage; +- struct sieve_binary_header *header = &sbin->header; +- uint32_t flags = 0; + + if (storage == NULL) + return; +- if (sieve_rusage_storage_load(storage, &rusage, &flags) <= 0) ++ if (sieve_rusage_storage_load(storage, &rusage) <= 0) + return; + + sbin->persisted_rusage.cpu_time_msecs = rusage.cpu_time_msecs; + sbin->persisted_rusage.update_time = ioloop_time; +- header->flags |= (flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT); + sieve_resource_usage_init(&sbin->rusage); + sbin->rusage_updated = FALSE; + } +@@ -241,16 +238,10 @@ void sieve_binary_get_resource_usage(str + + bool sieve_binary_check_resource_usage(struct sieve_binary *sbin) + { +- struct sieve_binary_header *header = &sbin->header; + struct sieve_resource_usage rusage; + + sieve_binary_get_resource_usage(sbin, &rusage); +- +- if (sieve_resource_usage_is_excessive(sbin->svinst, &rusage)) { +- header->flags |= SIEVE_BINARY_FLAG_RESOURCE_LIMIT; +- return FALSE; +- } +- return TRUE; ++ return !sieve_resource_usage_is_excessive(sbin->svinst, &rusage); + } + + bool sieve_binary_record_resource_usage( +Index: trixie/pigeonhole/src/lib-sieve/sieve-rusage.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-rusage.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-rusage.c +@@ -26,12 +26,11 @@ + #define SIEVE_RUSAGE_LOCK_TIMEOUT 10 + #define SIEVE_RUSAGE_LOCK_STALE_TIMEOUT 60 + +-/* Maximum size of the on-disk file. Single-line ASCII format with four ++/* Maximum size of the on-disk file. Single-line ASCII format with three + space-separated tokens; anything larger is treated as corrupt. */ + #define SIEVE_RUSAGE_MAX_FILE_SIZE 128 + + struct sieve_rusage_record { +- uint32_t flags; + uint32_t cpu_time_msecs; + time_t update_time; + }; +@@ -72,13 +71,11 @@ sieve_rusage_parse(const char *line, str + strcmp(tokens[0], SIEVE_RUSAGE_VERSION_TAG) != 0) + break; + if (tokens[1] == NULL || tokens[2] == NULL || +- tokens[3] == NULL || tokens[4] != NULL) ++ tokens[3] != NULL) + break; +- if (str_to_uint32(tokens[1], &rec->flags) < 0) ++ if (str_to_uint32(tokens[1], &cpu_secs) < 0) + break; +- if (str_to_uint32(tokens[2], &cpu_secs) < 0) +- break; +- if (str_to_time(tokens[3], &rec->update_time) < 0) ++ if (str_to_time(tokens[2], &rec->update_time) < 0) + break; + if (cpu_secs > UINT32_MAX / 1000) + rec->cpu_time_msecs = UINT32_MAX; +@@ -148,14 +145,12 @@ sieve_rusage_dotlock_settings(struct dot + } + + int sieve_rusage_storage_load(struct sieve_storage *storage, +- struct sieve_resource_usage *rusage_r, +- uint32_t *flags_r) ++ struct sieve_resource_usage *rusage_r) + { + struct sieve_rusage_record rec; + unsigned int timeout; + + sieve_resource_usage_init(rusage_r); +- *flags_r = 0; + + if (storage->rusage_path == NULL) + return 0; +@@ -171,13 +166,11 @@ int sieve_rusage_storage_load(struct sie + } + + rusage_r->cpu_time_msecs = rec.cpu_time_msecs; +- *flags_r = rec.flags; + return 1; + } + + int sieve_rusage_storage_add(struct sieve_storage *storage, +- const struct sieve_resource_usage *delta_rusage, +- uint32_t flags_to_set) ++ const struct sieve_resource_usage *delta_rusage) + { + struct sieve_rusage_record rec; + struct dotlock_settings dlset; +@@ -226,7 +219,6 @@ int sieve_rusage_storage_add(struct siev + (ioloop_time - rec.update_time) > (time_t)timeout) { + /* decayed: discard previous values */ + rec.cpu_time_msecs = 0; +- rec.flags = 0; + } + + old_cpu = rec.cpu_time_msecs; +@@ -234,7 +226,6 @@ int sieve_rusage_storage_add(struct siev + rec.cpu_time_msecs = UINT32_MAX; + else + rec.cpu_time_msecs = old_cpu + delta_rusage->cpu_time_msecs; +- rec.flags |= flags_to_set; + rec.update_time = ioloop_time; + + /* Round CPU time up to whole seconds for persistence. */ +@@ -244,8 +235,8 @@ int sieve_rusage_storage_add(struct siev + cpu_secs = (rec.cpu_time_msecs + 999) / 1000; + + out = t_str_new(64); +- str_printfa(out, "%s %u %u %lld\n", SIEVE_RUSAGE_VERSION_TAG, +- rec.flags, cpu_secs, (long long)rec.update_time); ++ str_printfa(out, "%s %u %lld\n", SIEVE_RUSAGE_VERSION_TAG, ++ cpu_secs, (long long)rec.update_time); + + wret = write(fd, str_data(out), str_len(out)); + if (wret != (ssize_t)str_len(out)) { +Index: trixie/pigeonhole/src/lib-sieve/sieve-rusage.h +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-rusage.h ++++ trixie/pigeonhole/src/lib-sieve/sieve-rusage.h +@@ -16,17 +16,13 @@ void sieve_rusage_storage_init(struct si + + /* Load persisted resource usage from the per-user file. Returns 1 on success, + 0 if the storage has no rusage file (no INBOX namespace path), -1 on error. +- Applies the resource_usage_timeout decay. flags_r receives sieve binary +- header flags (RESOURCE_LIMIT) that were persisted. */ ++ Applies the resource_usage_timeout decay. */ + int sieve_rusage_storage_load(struct sieve_storage *storage, +- struct sieve_resource_usage *rusage_r, +- uint32_t *flags_r); ++ struct sieve_resource_usage *rusage_r); + +-/* Atomically add delta_rusage to and OR flags_to_set into the persisted +- per-user rusage file. Read-modify-write under fcntl WRLCK. Returns 1 on +- success, 0 if no rusage file is configured, -1 on error. */ ++/* Atomically add delta_rusage to the persisted per-user rusage file. ++ Returns 1 on success, 0 if no rusage file is configured, -1 on error. */ + int sieve_rusage_storage_add(struct sieve_storage *storage, +- const struct sieve_resource_usage *delta_rusage, +- uint32_t flags_to_set); ++ const struct sieve_resource_usage *delta_rusage); + + #endif diff --git a/patches/0004-lib-var-expand-Change-escape-func-signature-to-retur.patch b/patches/0004-lib-var-expand-Change-escape-func-signature-to-retur.patch new file mode 100644 index 0000000..3211d30 --- /dev/null +++ b/patches/0004-lib-var-expand-Change-escape-func-signature-to-retur.patch @@ -0,0 +1,305 @@ +From d7a040f5b0230368dd97112daf7a710446fdfd16 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Thu, 21 May 2026 11:00:56 +0300 +Subject: [PATCH 4/6] lib-var-expand: Change escape func signature to return + int with error_r + +Wire error propagation in var_expand_program_execute_one_real() so a +failing escape function causes the expansion to return -1. + +Update all implementations (passdb-sql, userdb-sql, db-ldap, dict-ldap, +auth-request-var-expand) and replace the unsafe auth_request_escape_func_t +cast in auth-request-var-expand.c with a proper bridge wrapper. +--- + src/auth/auth-request-var-expand.c | 13 +++++++++++-- + src/auth/db-ldap.c | 12 ++++++++---- + src/auth/db-ldap.h | 4 ++-- + src/auth/passdb-sql.c | 6 ++++-- + src/auth/test-auth-request-var-expand.c | 16 ++++++++++++---- + src/auth/test-ldap.c | 5 +++-- + src/auth/userdb-sql.c | 6 ++++-- + src/lib-dict-backend/dict-ldap.c | 6 ++++-- + src/lib-var-expand/expansion-filter.c | 7 +++++-- + src/lib-var-expand/expansion-program.c | 11 +++++++---- + src/lib-var-expand/test-var-expand.c | 6 ++++-- + src/lib-var-expand/var-expand.h | 7 ++++--- + 12 files changed, 68 insertions(+), 31 deletions(-) + +Index: trixie/src/auth/auth-request-var-expand.c +=================================================================== +--- trixie.orig/src/auth/auth-request-var-expand.c ++++ trixie/src/auth/auth-request-var-expand.c +@@ -55,6 +55,15 @@ escape_none(const char *string, + return string; + } + ++static int ++auth_request_escape_wrapper(const char *input, const char **output_r, ++ void *context, const char **error_r ATTR_UNUSED) ++{ ++ const struct auth_request_var_expand_ctx *ctx = context; ++ *output_r = ctx->escape_func(input, ctx->auth_request); ++ return 0; ++} ++ + const char * + auth_request_str_escape(const char *string, + const struct auth_request *request ATTR_UNUSED) +@@ -242,9 +251,9 @@ int auth_request_var_expand_with_table(s + const struct var_expand_params params = { + .table = table, + .providers = auth_request_var_expand_providers, +- .escape_func = (var_expand_escape_func_t *)ctx.escape_func, ++ .escape_func = auth_request_escape_wrapper, + .context = &ctx, +- .escape_context = (void *)auth_request, ++ .escape_context = &ctx, + .event = auth_request->event, + }; + +Index: trixie/src/auth/db-ldap.c +=================================================================== +--- trixie.orig/src/auth/db-ldap.c ++++ trixie/src/auth/db-ldap.c +@@ -1138,7 +1138,8 @@ void db_ldap_get_attribute_names(pool_t + *sensitive_r = array_front(&sensitive_attr_names); + } + +-const char *ldap_escape(const char *input, void *context ATTR_UNUSED) ++int ldap_escape(const char *input, const char **output_r, ++ void *context ATTR_UNUSED, const char **error_r ATTR_UNUSED) + { + /* This function escapes both LDAP filters and LDAP DNs. This works, + because both allow using the method of escaping any characters. +@@ -1195,8 +1196,10 @@ const char *ldap_escape(const char *inpu + (required by DN) */ + if (pos > 0 && input[pos - 1] == ' ') + pos--; +- else +- return input; ++ else { ++ *output_r = input; ++ return 0; ++ } + } + } + +@@ -1211,7 +1214,8 @@ const char *ldap_escape(const char *inpu + pos--; + } while (pos < input_len); + str_append_data(str, input, pos); +- return str_c(str); ++ *output_r = str_c(str); ++ return 0; + } + + static bool +Index: trixie/src/auth/db-ldap.h +=================================================================== +--- trixie.orig/src/auth/db-ldap.h ++++ trixie/src/auth/db-ldap.h +@@ -169,8 +169,8 @@ void db_ldap_connect_delayed(struct ldap + + void db_ldap_enable_input(struct ldap_connection *conn, bool enable); + +-const char *ldap_dn_escape(const char *str, void *context); +-const char *ldap_escape(const char *str, void *context); ++int ldap_dn_escape(const char *str, const char **output_r, void *context, const char **error_r); ++int ldap_escape(const char *str, const char **output_r, void *context, const char **error_r); + const char *ldap_get_error(struct ldap_connection *conn); + + struct db_ldap_result_iterate_context * +Index: trixie/src/auth/passdb-sql.c +=================================================================== +--- trixie.orig/src/auth/passdb-sql.c ++++ trixie/src/auth/passdb-sql.c +@@ -167,10 +167,12 @@ static void sql_query_callback(struct sq + auth_request_unref(&auth_request); + } + +-static const char *passdb_sql_escape(const char *str, void *context) ++static int passdb_sql_escape(const char *str, const char **output_r, ++ void *context, const char **error_r ATTR_UNUSED) + { + struct sql_db *db = context; +- return sql_escape_string(db, str); ++ *output_r = sql_escape_string(db, str); ++ return 0; + } + + static void sql_lookup_pass(struct passdb_sql_request *sql_request) +Index: trixie/src/auth/test-auth-request-var-expand.c +=================================================================== +--- trixie.orig/src/auth/test-auth-request-var-expand.c ++++ trixie/src/auth/test-auth-request-var-expand.c +@@ -70,6 +70,14 @@ test_escape(const char *string, const st + return dest; + } + ++static int ++test_escape_varexpand(const char *string, const char **output_r, ++ void *context, const char **error_r ATTR_UNUSED) ++{ ++ *output_r = test_escape(string, context); ++ return 0; ++} ++ + static bool test_empty_request(string_t *str, const char *input) + { + const struct var_expand_params params = { +@@ -102,7 +110,7 @@ static void test_auth_request_var_expand + + const struct var_expand_params params = { + .table = auth_request_get_var_expand_table(&test_request), +- .escape_func = (var_expand_escape_func_t *)test_escape, ++ .escape_func = test_escape_varexpand, + .escape_context = &test_request, + }; + +@@ -129,7 +137,7 @@ static void test_auth_request_var_expand + + struct var_expand_params params = { + .table = auth_request_get_var_expand_table(&test_request), +- .escape_func = (var_expand_escape_func_t *)test_escape, ++ .escape_func = test_escape_varexpand, + .escape_context = &test_request + }; + test_assert(var_expand(str, test_input, ¶ms, &error) == 0); +@@ -169,7 +177,7 @@ static void test_auth_request_var_expand + + const struct var_expand_params params = { + .table = auth_request_get_var_expand_table(&test_request), +- .escape_func = (var_expand_escape_func_t *)test_escape, ++ .escape_func = test_escape_varexpand, + .escape_context = &test_request, + }; + +@@ -200,7 +208,7 @@ static void test_auth_request_var_expand + test_request.fields.user = t_strdup_noconst(tests[i].username); + const struct var_expand_params params = { + .table = auth_request_get_var_expand_table(&test_request), +- .escape_func = (var_expand_escape_func_t *)test_escape, ++ .escape_func = test_escape_varexpand, + .escape_context = &test_request, + }; + str_truncate(str, 0); +Index: trixie/src/auth/test-ldap.c +=================================================================== +--- trixie.orig/src/auth/test-ldap.c ++++ trixie/src/auth/test-ldap.c +@@ -25,8 +25,9 @@ static void test_ldap_escape(void) + }; + test_begin("ldap_escape()"); + for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) { +- test_assert_strcmp_idx(ldap_escape(tests[i].input, NULL), +- tests[i].output, i); ++ const char *output, *error; ++ test_assert(ldap_escape(tests[i].input, &output, NULL, &error) == 0); ++ test_assert_strcmp_idx(output, tests[i].output, i); + } + test_end(); + } +Index: trixie/src/auth/userdb-sql.c +=================================================================== +--- trixie.orig/src/auth/userdb-sql.c ++++ trixie/src/auth/userdb-sql.c +@@ -112,10 +112,12 @@ static void sql_query_callback(struct sq + i_free(sql_request); + } + +-static const char *userdb_sql_escape(const char *str, void *context) ++static int userdb_sql_escape(const char *str, const char **output_r, ++ void *context, const char **error_r ATTR_UNUSED) + { + struct sql_db *db = context; +- return sql_escape_string(db, str); ++ *output_r = sql_escape_string(db, str); ++ return 0; + } + + static void userdb_sql_lookup(struct auth_request *auth_request, +Index: trixie/src/lib-dict-backend/dict-ldap.c +=================================================================== +--- trixie.orig/src/lib-dict-backend/dict-ldap.c ++++ trixie/src/lib-dict-backend/dict-ldap.c +@@ -139,7 +139,8 @@ int dict_ldap_connect(struct ldap_dict * + #define IS_LDAP_ESCAPED_CHAR(c) \ + ((((unsigned char)(c)) & 0x80) != 0 || strchr(LDAP_ESCAPE_CHARS, (c)) != NULL) + +-static const char *ldap_escape(const char *str, void *context ATTR_UNUSED) ++static int ldap_escape(const char *str, const char **output_r, ++ void *context ATTR_UNUSED, const char **error_r ATTR_UNUSED) + { + string_t *ret = NULL; + +@@ -154,7 +155,8 @@ static const char *ldap_escape(const cha + str_append_c(ret, *p); + } + +- return ret == NULL ? str : str_c(ret); ++ *output_r = ret == NULL ? str : str_c(ret); ++ return 0; + } + + static +Index: trixie/src/lib-var-expand/expansion-program.c +=================================================================== +--- trixie.orig/src/lib-var-expand/expansion-program.c ++++ trixie/src/lib-var-expand/expansion-program.c +@@ -142,9 +142,11 @@ int var_expand_program_execute(string_t + if (state.transfer_set) { + if (!program->only_literal && !state.transfer_safe && + params->escape_func != NULL) { +- str_append(state.result, +- params->escape_func(str_c(state.transfer), +- params->escape_context)); ++ const char *escaped; ++ if (params->escape_func(str_c(state.transfer), &escaped, ++ params->escape_context, error_r) < 0) ++ return -1; ++ str_append(state.result, escaped); + } else + str_append_str(state.result, state.transfer); + } else { +Index: trixie/src/lib-var-expand/test-var-expand.c +=================================================================== +--- trixie.orig/src/lib-var-expand/test-var-expand.c ++++ trixie/src/lib-var-expand/test-var-expand.c +@@ -586,7 +586,8 @@ static void test_var_expand_tables_arr(v + test_end(); + } + +-static const char *test_escape(const char *str, void *context) ++static int test_escape(const char *str, const char **output_r, ++ void *context, const char **error_r ATTR_UNUSED) + { + const char *escape_chars = context; + string_t *dest = t_str_new(strlen(str) + 2); +@@ -601,7 +602,8 @@ static const char *test_escape(const cha + } + } + str_append_c(dest, '\''); +- return str_c(dest); ++ *output_r = str_c(dest); ++ return 0; + } + + static void test_var_expand_escape(void) +Index: trixie/src/lib-var-expand/var-expand.h +=================================================================== +--- trixie.orig/src/lib-var-expand/var-expand.h ++++ trixie/src/lib-var-expand/var-expand.h +@@ -11,9 +11,10 @@ + */ + typedef int value_provider_func_t(const char *key, const char **value_r, + void *context, const char **error_r); +-/* Used for escaping values, gets given string to escape and context, +- must return escaped string. */ +-typedef const char *var_expand_escape_func_t(const char *str, void *context); ++/* Used for escaping values. On success sets output_r and returns 0. ++ On failure sets error_r and returns -1. */ ++typedef int var_expand_escape_func_t(const char *input, const char **output_r, ++ void *context, const char **error_r); + + struct var_expand_parser_state; + struct var_expand_program; diff --git a/patches/0004-submission-login-client-authenticate-Reply-421-4.7.0.patch b/patches/0004-submission-login-client-authenticate-Reply-421-4.7.0.patch new file mode 100644 index 0000000..f5bbc7a --- /dev/null +++ b/patches/0004-submission-login-client-authenticate-Reply-421-4.7.0.patch @@ -0,0 +1,51 @@ +From 4e968e6c8614a935a83024ba4ac9a5c9b6c6b7ff Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 7 May 2026 10:58:12 +0000 +Subject: [PATCH 4/5] submission-login: client-authenticate - Reply 421 4.7.0 + on mail_max_userip_connections + +Until now the connection limit was reported via the same 454 4.7.0 reply +that is used for generic temporary authentication failures. That makes +proxies (including Dovecot's own submission proxy) treat the rejection as +a transient auth error and retry, which is futile when the limit is hit +and only obscures the actual cause in the proxy log. + +Reply with 421 4.7.0 instead. RFC 5321 Section 4.2.1 specifies 421 as +"service shutting down, closing transmission channel", which is the +right signal for "do not retry on this connection". The 421 + 4.7.0 +combination is unique among the 421 replies emitted by submission and is +used by the submission proxy to recognize this specifically as a +connection-limit reply rather than a generic 421 internal/shutdown. +--- + src/submission-login/client-authenticate.c | 19 ++++++++++++++++++- + 1 file changed, 18 insertions(+), 1 deletion(-) + +Index: trixie/src/submission-login/client-authenticate.c +=================================================================== +--- trixie.orig/src/submission-login/client-authenticate.c ++++ trixie/src/submission-login/client-authenticate.c +@@ -151,6 +151,24 @@ void submission_client_auth_result(struc + */ + smtp_server_reply(cmd, 454, "4.7.0", "%s", text); + break; ++ case CLIENT_AUTH_RESULT_LIMIT_REACHED: ++ /* The user has too many concurrent connections. Reply with ++ 421 4.7.0: 421 means "service shutting down, closing ++ transmission channel" (RFC 5321 Section 4.2.1) and ++ signals the client that retrying on this same connection ++ is pointless. The proxy uses the 421 + 4.7.0 combination ++ to recognize this specifically as a connection-limit ++ response (rather than a generic 421 internal/shutdown ++ reply) and avoid reconnecting. ++ ++ Use reply_immediate() rather than the queued ++ smtp_server_reply() because the caller (sasl-server) ++ immediately tears the connection down after this returns, ++ which would abort a queued reply before it reaches the ++ wire. */ ++ smtp_server_connection_reply_immediate(subm_client->conn, ++ 421, "4.7.0 %s", text); ++ break; + case CLIENT_AUTH_RESULT_ABORTED: + /* RFC4954, Section 4: + diff --git a/patches/0005-imap-Add-imap_compress_on_proxy-hidden-setting.patch b/patches/0005-imap-Add-imap_compress_on_proxy-hidden-setting.patch new file mode 100644 index 0000000..b974c65 --- /dev/null +++ b/patches/0005-imap-Add-imap_compress_on_proxy-hidden-setting.patch @@ -0,0 +1,62 @@ +From c27391fb8528e95e2b2b30c9694aab1ab4341b14 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 4 Jun 2025 17:05:36 +0300 +Subject: [PATCH 5/6] imap: Add imap_compress_on_proxy hidden setting + +Keep it disabled by default for now. Once we're sure COMPRESS on proxy +works properly we'll enable it again. +--- + src/imap/cmd-compress.c | 3 ++- + src/imap/imap-settings.c | 2 ++ + src/imap/imap-settings.h | 1 + + 3 files changed, 5 insertions(+), 1 deletion(-) + +diff --git a/src/imap/cmd-compress.c b/src/imap/cmd-compress.c +index 18aa988892..0367cc6f64 100644 +--- a/src/imap/cmd-compress.c ++++ b/src/imap/cmd-compress.c +@@ -87,7 +87,8 @@ bool cmd_compress(struct client_command_context *cmd) + return TRUE; + } + +- if (client->multiplex_output != NULL) { ++ if (client->multiplex_output != NULL && ++ client->set->imap_compress_on_proxy) { + /* Let imap-login process handle the COMPRESS. It's the one + that will send the tagged reply to the client. */ + client->compress_handler = handler; +diff --git a/src/imap/imap-settings.c b/src/imap/imap-settings.c +index aa0cd747cb..3e2561ccd2 100644 +--- a/src/imap/imap-settings.c ++++ b/src/imap/imap-settings.c +@@ -81,6 +81,7 @@ static const struct setting_define imap_setting_defines[] = { + DEF(ENUM, imap_fetch_failure), + DEF(BOOL, imap_metadata), + DEF(BOOL, imap_literal_minus), ++ DEF(BOOL_HIDDEN, imap_compress_on_proxy), + DEF(BOOL, mail_utf8_extensions), + #ifdef BUILD_IMAP_HIBERNATE + DEF(TIME, imap_hibernate_timeout), +@@ -115,6 +116,7 @@ static const struct imap_settings imap_default_settings = { + .imap_fetch_failure = "disconnect-immediately:disconnect-after:no-after", + .imap_metadata = FALSE, + .imap_literal_minus = FALSE, ++ .imap_compress_on_proxy = FALSE, + .mail_utf8_extensions = FALSE, + #ifdef DOVECOT_PRO_EDITION + .imap_hibernate_timeout = 30, +diff --git a/src/imap/imap-settings.h b/src/imap/imap-settings.h +index 202ea45a30..347d85cec7 100644 +--- a/src/imap/imap-settings.h ++++ b/src/imap/imap-settings.h +@@ -34,6 +34,7 @@ struct imap_settings { + const char *imap_fetch_failure; + bool imap_metadata; + bool imap_literal_minus; ++ bool imap_compress_on_proxy; + bool mail_utf8_extensions; + unsigned int imap_hibernate_timeout; + ARRAY_TYPE(const_string) imap_id_send; +-- +2.47.3 + diff --git a/patches/0005-lib-Add-comments-about-memory-allocations-and-string.patch b/patches/0005-lib-Add-comments-about-memory-allocations-and-string.patch new file mode 100644 index 0000000..e2f7363 --- /dev/null +++ b/patches/0005-lib-Add-comments-about-memory-allocations-and-string.patch @@ -0,0 +1,81 @@ +From 626a19e307e915b2c545cad226aa57aec8620906 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 3 Jun 2026 21:14:51 +0000 +Subject: [PATCH 5/5] lib: Add comments about memory allocations and string + functions preserving errno + +--- + src/lib/data-stack.h | 5 +++++ + src/lib/imem.h | 3 +++ + src/lib/mempool.h | 6 ++++++ + src/lib/strfuncs.h | 7 +++++++ + 4 files changed, 21 insertions(+) + +diff --git a/src/lib/data-stack.h b/src/lib/data-stack.h +index 7ff66c4ef4..e0febe494f 100644 +--- a/src/lib/data-stack.h ++++ b/src/lib/data-stack.h +@@ -31,6 +31,11 @@ + overflows. + */ + ++/* All data stack allocations - t_malloc(), t_malloc0(), t_buffer_get(), ++ t_try_realloc() etc. - preserve errno. It is therefore safe to allocate ++ temporary memory between a failing syscall and reading errno (or ++ formatting "%m"). */ ++ + #ifndef STATIC_CHECKER + typedef unsigned int data_stack_frame_t; + #else +diff --git a/src/lib/imem.h b/src/lib/imem.h +index ed8c2eb581..6635bc018a 100644 +--- a/src/lib/imem.h ++++ b/src/lib/imem.h +@@ -3,6 +3,9 @@ + + /* For easy allocation of memory from default memory pool. */ + ++/* Like all pool allocations, i_malloc()/i_realloc()/i_free() and the ++ i_strdup*() helpers preserve errno (see mempool.h). */ ++ + extern pool_t default_pool; + + #define i_new(type, count) p_new(default_pool, type, count) +diff --git a/src/lib/mempool.h b/src/lib/mempool.h +index 3b9872cb20..37ecff4c7c 100644 +--- a/src/lib/mempool.h ++++ b/src/lib/mempool.h +@@ -19,6 +19,12 @@ + zeroed, it will cost only a few CPU cycles and may well save some debug + time. */ + ++/* All pool memory operations - p_malloc(), p_realloc(), p_free() and the ++ p_new()/p_strdup*() helpers built on them - preserve errno. This means an ++ allocation between a failing syscall and reading errno (e.g. when building ++ an error string with "%m") will not clobber errno. The same guarantee holds ++ for the t_* (data stack) and i_* (default pool) allocators. */ ++ + typedef struct pool *pool_t; + + struct pool_vfuncs { +diff --git a/src/lib/strfuncs.h b/src/lib/strfuncs.h +index 715af58d8e..e6daa14847 100644 +--- a/src/lib/strfuncs.h ++++ b/src/lib/strfuncs.h +@@ -13,6 +13,13 @@ extern const char *const empty_str_array[]; + int i_snprintf(char *dest, size_t max_chars, const char *format, ...) + ATTR_FORMAT(3, 4); + ++/* The p_/t_/i_ strdup, strconcat and *printf helpers below preserve errno. ++ So this pattern is safe - errno still refers to the failed syscall when ++ "%m" is expanded: ++ ++ if (syscall(...) < 0) ++ error = t_strdup_printf("syscall() failed: %m"); */ ++ + char *p_strdup(pool_t pool, const char *str) ATTR_MALLOC; + void *p_memdup(pool_t pool, const void *data, size_t size) ATTR_MALLOC; + /* return NULL if str = "" */ +-- +2.39.5 + diff --git a/patches/0005-lib-index-mail-index-strmap-Fix-OOB-read-from-trunca.patch b/patches/0005-lib-index-mail-index-strmap-Fix-OOB-read-from-trunca.patch new file mode 100644 index 0000000..45b2ff0 --- /dev/null +++ b/patches/0005-lib-index-mail-index-strmap-Fix-OOB-read-from-trunca.patch @@ -0,0 +1,27 @@ +From b0f98d919ca586171c8149e52326e6f0c0ec5252 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 17 Aug 2026 11:13:18 +0300 +Subject: [PATCH 05/12] lib-index: mail-index-strmap - Fix OOB read from + truncated record size + +--- + src/lib-index/mail-index-strmap.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/src/lib-index/mail-index-strmap.c b/src/lib-index/mail-index-strmap.c +index fb92517068..9ce2de9103 100644 +--- a/src/lib-index/mail-index-strmap.c ++++ b/src/lib-index/mail-index-strmap.c +@@ -425,6 +425,9 @@ mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx, + if (mail_index_strmap_read_packed(ctx, &n) <= 0) + return -1; + count = n < 2 ? n + 1 : n; ++ /* check that rec_size fits */ ++ if (UINT_MAX / (sizeof(ctx->rec.str_idx) + sizeof(*crc32_r)) < count) ++ return -1; + ctx->view->total_ref_count += count; + + ctx->rec_size = count * (sizeof(ctx->rec.str_idx) + sizeof(*crc32_r)); +-- +2.47.3 + diff --git a/patches/0005-lib-mail-Add-count-field-to-struct-message_address_l.patch b/patches/0005-lib-mail-Add-count-field-to-struct-message_address_l.patch new file mode 100644 index 0000000..6679ee3 --- /dev/null +++ b/patches/0005-lib-mail-Add-count-field-to-struct-message_address_l.patch @@ -0,0 +1,56 @@ +From ef2cddece8101876c0e756536e6ef358c780dc07 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 17 Apr 2026 15:46:10 +0200 +Subject: [PATCH 05/14] lib-mail: Add count field to struct + message_address_list + +Add a count field and increment it in add_address() so callers can read +the number of parsed addresses directly from the list struct without +walking the linked list. + +Co-Authored-By: Claude Sonnet 4.6 +--- + src/lib-mail/message-address.c | 1 + + src/lib-mail/message-address.h | 1 + + src/lib-mail/message-part-data.c | 1 + + 3 files changed, 3 insertions(+) + +diff --git a/src/lib-mail/message-address.c b/src/lib-mail/message-address.c +index 675e652e04..967cc518bf 100644 +--- a/src/lib-mail/message-address.c ++++ b/src/lib-mail/message-address.c +@@ -30,6 +30,7 @@ static void add_address(struct message_address_parser_context *ctx) + i_zero(&ctx->addr); + + DLLIST2_APPEND(&ctx->addr_list.head, &ctx->addr_list.tail, addr); ++ ctx->addr_list.count++; + } + + /* quote with "" and escape all '\', '"' and "'" characters if need */ +diff --git a/src/lib-mail/message-address.h b/src/lib-mail/message-address.h +index 224f7a7560..8b99ce1401 100644 +--- a/src/lib-mail/message-address.h ++++ b/src/lib-mail/message-address.h +@@ -33,6 +33,7 @@ struct message_address { + + struct message_address_list { + struct message_address *head, *tail; ++ unsigned int count; + }; + + /* Parse message addresses from given data. Note that giving an empty string +diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c +index 895bf88e85..acd50e5210 100644 +--- a/src/lib-mail/message-part-data.c ++++ b/src/lib-mail/message-part-data.c +@@ -247,6 +247,7 @@ void message_part_envelope_parse_from_header(pool_t pool, + validated while MUA only shows the second From header. */ + DLLIST2_JOIN(&addr_p->head, &addr_p->tail, + &new_addr.head, &new_addr.tail); ++ addr_p->count += new_addr.count; + } else if (str_p != NULL) { + *str_p = message_header_strdup(pool, hdr->full_value, + hdr->full_value_len); +-- +2.47.3 + diff --git a/patches/0005-lib-sieve-file-storage-Remove-orphan-compiled-binari.patch b/patches/0005-lib-sieve-file-storage-Remove-orphan-compiled-binari.patch new file mode 100644 index 0000000..b1114be --- /dev/null +++ b/patches/0005-lib-sieve-file-storage-Remove-orphan-compiled-binari.patch @@ -0,0 +1,55 @@ +From 88470b80fb4079cd2207bc8c4e986b816218421e Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 5 May 2026 16:51:27 +0000 +Subject: [PATCH 5/7] lib-sieve: file storage: Remove orphan compiled binaries + on delete and rename + +DELETESCRIPT and RENAMESCRIPT only operated on the .sieve file and +left the matching compiled binary (.svbin) on disk. The orphan was +never referenced again under that name, so it accumulated as wasted +disk space until something else (manual cleanup, account removal) +removed it. + +Unlink fscript->bin_path alongside the .sieve operation. The unlink is +best-effort: failures other than ENOENT are logged but do not fail the +DELETE/RENAME. +--- + src/lib-sieve/storage/file/sieve-file-script.c | 18 ++++++++++++++++++ + 1 file changed, 18 insertions(+) + +Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c ++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c +@@ -659,6 +659,14 @@ static int sieve_file_storage_script_del + fscript->path); + } + } ++ ++ /* Best-effort: drop the compiled binary as well so it does not ++ linger as a stale orphan. */ ++ if (fscript->bin_path != NULL && ++ unlink(fscript->bin_path) < 0 && errno != ENOENT) { ++ e_error(script->event, ++ "unlink(%s) failed: %m", fscript->bin_path); ++ } + return ret; + } + +@@ -791,6 +799,16 @@ sieve_file_storage_script_rename(struct + fscript->path); + } + ++ /* Drop the old compiled binary; the renamed ++ script will recompile under its new name. */ ++ if (fscript->bin_path != NULL && ++ unlink(fscript->bin_path) < 0 && ++ errno != ENOENT) { ++ e_error(script->event, ++ "unlink(%s) failed: %m", ++ fscript->bin_path); ++ } ++ + if (script->name != NULL && *script->name != '\0') + script->name = p_strdup(script->pool, newname); + fscript->path = p_strdup(script->pool, newpath); diff --git a/patches/0005-lib-sql-Make-escape_string-return-int-with-error_r-f.patch b/patches/0005-lib-sql-Make-escape_string-return-int-with-error_r-f.patch new file mode 100644 index 0000000..6bbd491 --- /dev/null +++ b/patches/0005-lib-sql-Make-escape_string-return-int-with-error_r-f.patch @@ -0,0 +1,629 @@ +From 4e83f5367d2b897430c91c8be9f0830b16e3d90f Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Fri, 29 May 2026 11:22:57 +0000 +Subject: [PATCH 5/6] lib-sql: Make escape_string return int with error_r, fail + instead of unsafe fallback + +Change escape_string driver vfunc and sql_escape_string() to return int +with separate error_r output parameter. On failure (e.g. not connected), +return -1 instead of falling back to unsafe escaping. + +Move escaping from sql_statement_bind_str() to sql_statement_get_query() +so errors can be propagated to callers. Add failed_error field to +sql_transaction_context for deferred error reporting at commit time. +--- + src/auth/passdb-sql.c | 5 +- + src/auth/userdb-sql.c | 5 +- + src/lib-sql/driver-cassandra.c | 52 ++++++++++++----- + src/lib-sql/driver-mysql.c | 20 +++---- + src/lib-sql/driver-pgsql.c | 27 ++++++--- + src/lib-sql/driver-sqlite.c | 8 ++- + src/lib-sql/driver-sqlpool.c | 10 ++-- + src/lib-sql/driver-test.c | 27 +++++---- + src/lib-sql/sql-api-private.h | 8 ++- + src/lib-sql/sql-api.c | 103 +++++++++++++++++++++++++++------ + src/lib-sql/sql-api.h | 3 +- + 11 files changed, 188 insertions(+), 80 deletions(-) + +Index: trixie/src/auth/passdb-sql.c +=================================================================== +--- trixie.orig/src/auth/passdb-sql.c ++++ trixie/src/auth/passdb-sql.c +@@ -168,11 +168,10 @@ static void sql_query_callback(struct sq + } + + static int passdb_sql_escape(const char *str, const char **output_r, +- void *context, const char **error_r ATTR_UNUSED) ++ void *context, const char **error_r) + { + struct sql_db *db = context; +- *output_r = sql_escape_string(db, str); +- return 0; ++ return sql_escape_string(db, str, output_r, error_r); + } + + static void sql_lookup_pass(struct passdb_sql_request *sql_request) +Index: trixie/src/auth/userdb-sql.c +=================================================================== +--- trixie.orig/src/auth/userdb-sql.c ++++ trixie/src/auth/userdb-sql.c +@@ -113,11 +113,10 @@ static void sql_query_callback(struct sq + } + + static int userdb_sql_escape(const char *str, const char **output_r, +- void *context, const char **error_r ATTR_UNUSED) ++ void *context, const char **error_r) + { + struct sql_db *db = context; +- *output_r = sql_escape_string(db, str); +- return 0; ++ return sql_escape_string(db, str, output_r, error_r); + } + + static void userdb_sql_lookup(struct auth_request *auth_request, +Index: trixie/src/lib-sql/driver-cassandra.c +=================================================================== +--- trixie.orig/src/lib-sql/driver-cassandra.c ++++ trixie/src/lib-sql/driver-cassandra.c +@@ -830,22 +830,26 @@ static void driver_cassandra_disconnect( + driver_cassandra_close(db, "Disconnected"); + } + +-static const char * ++static int + driver_cassandra_escape_string(struct sql_db *db ATTR_UNUSED, +- const char *string) ++ const char *string, const char **output_r, ++ const char **error_r ATTR_UNUSED) + { + string_t *escaped; + unsigned int i; + +- if (strchr(string, '\'') == NULL) +- return string; ++ if (strchr(string, '\'') == NULL) { ++ *output_r = string; ++ return 0; ++ } + escaped = t_str_new(strlen(string)+10); + for (i = 0; string[i] != '\0'; i++) { + if (string[i] == '\'') + str_append_c(escaped, '\''); + str_append_c(escaped, string[i]); + } +- return str_c(escaped); ++ *output_r = str_c(escaped); ++ return 0; + } + + static void +@@ -2192,8 +2196,13 @@ static void cassandra_transaction_finish + if (stmt->prep == NULL) + have_nonprepared = TRUE; + if (stmt->cass_stmt == NULL) { +- stmt->cass_stmt = cass_statement_new( +- sql_statement_get_query(&stmt->stmt), 0); ++ const char *query, *error; ++ if (sql_statement_get_query(&stmt->stmt, ++ &query, &error) < 0) { ++ cassandra_transaction_finish(ctx, error); ++ return; ++ } ++ stmt->cass_stmt = cass_statement_new(query, 0); + if (stmt->timestamp != 0) { + cass_statement_set_timestamp(stmt->cass_stmt, + stmt->timestamp); +@@ -2289,11 +2298,15 @@ driver_cassandra_try_commit_s(struct cas + i_panic("cassandra: sql_transaction_commit_s() not supported for prepared statements"); + } + ++ const char *query, *error; ++ if (sql_statement_get_query(&stmt->stmt, &query, &error) < 0) { ++ transaction_set_failed(ctx, error); ++ return; ++ } ++ + /* just a single query, send it */ + driver_cassandra_sync_init(db); +- result = driver_cassandra_sync_query(db, +- sql_statement_get_query(&stmt->stmt), +- ctx->query_type); ++ result = driver_cassandra_sync_query(db, query, ctx->query_type); + driver_cassandra_sync_deinit(db); + + if (sql_result_next_row(result) < 0) +@@ -2796,8 +2809,14 @@ driver_cassandra_statement_query(struct + } else { + /* Not a prepared statement. Generate a statement from + the query string. */ +- stmt->result->statement = +- cass_statement_new(sql_statement_get_query(_stmt), 0); ++ const char *query, *error; ++ if (sql_statement_get_query(_stmt, &query, &error) < 0) { ++ stmt->result->error = i_strdup(error); ++ result_finish(stmt->result); ++ cassandra_sql_statement_free(stmt); ++ return; ++ } ++ stmt->result->statement = cass_statement_new(query, 0); + stmt->result->timestamp = stmt->timestamp; + if (stmt->timestamp != 0) { + cass_statement_set_timestamp(stmt->result->statement, +@@ -2826,8 +2845,13 @@ driver_cassandra_update_stmt(struct sql_ + + i_assert(affected_rows == NULL); + +- if (!driver_cassandra_update_query_type(ctx, +- sql_statement_get_query(_stmt))) { ++ const char *query, *error; ++ if (sql_statement_get_query(_stmt, &query, &error) < 0) { ++ transaction_set_failed(ctx, error); ++ cassandra_sql_statement_free(stmt); ++ return; ++ } ++ if (!driver_cassandra_update_query_type(ctx, query)) { + cassandra_sql_statement_free(stmt); + return; + } +Index: trixie/src/lib-sql/driver-mysql.c +=================================================================== +--- trixie.orig/src/lib-sql/driver-mysql.c ++++ trixie/src/lib-sql/driver-mysql.c +@@ -462,8 +462,9 @@ static int driver_mysql_do_query(struct + return -1; + } + +-static const char * +-driver_mysql_escape_string(struct sql_db *_db, const char *string) ++static int ++driver_mysql_escape_string(struct sql_db *_db, const char *string, ++ const char **output_r, const char **error_r) + { + struct mysql_db *db = container_of(_db, struct mysql_db, api); + size_t len = strlen(string); +@@ -475,22 +476,15 @@ driver_mysql_escape_string(struct sql_db + } + + if (_db->state == SQL_DB_STATE_DISCONNECTED) { +- /* FIXME: we don't have a valid connection, so fallback +- to using default escaping. the next query will most +- likely fail anyway so it shouldn't matter that much +- what we return here.. Anyway, this API needs +- changing so that the escaping function could already +- fail the query reliably. */ +- to = t_buffer_get(len * 2 + 1); +- len = mysql_escape_string(to, string, len); +- t_buffer_alloc(len + 1); +- return to; ++ *error_r = SQL_ERRSTR_NOT_CONNECTED; ++ return -1; + } + + to = t_buffer_get(len * 2 + 1); + len = mysql_real_escape_string(db->mysql, to, string, len); + t_buffer_alloc(len + 1); +- return to; ++ *output_r = to; ++ return 0; + } + + static void driver_mysql_exec(struct sql_db *_db, const char *query) +Index: trixie/src/lib-sql/driver-pgsql.c +=================================================================== +--- trixie.orig/src/lib-sql/driver-pgsql.c ++++ trixie/src/lib-sql/driver-pgsql.c +@@ -718,8 +718,9 @@ static void do_query(struct pgsql_result + } + } + +-static const char * +-driver_pgsql_escape_string(struct sql_db *_db, const char *string) ++static int ++driver_pgsql_escape_string(struct sql_db *_db, const char *string, ++ const char **output_r, const char **error_r) + { + struct pgsql_db *db = (struct pgsql_db *)_db; + size_t len = strlen(string); +@@ -735,14 +736,24 @@ driver_pgsql_escape_string(struct sql_db + + to = t_buffer_get(len * 2 + 1); + len = PQescapeStringConn(db->pg, to, string, len, &error); +- } else +-#endif +- { +- to = t_buffer_get(len * 2 + 1); +- len = PQescapeString(to, string, len); ++ if (error != 0) { ++ *error_r = last_error(db); ++ return -1; ++ } ++ t_buffer_alloc(len + 1); ++ *output_r = to; ++ return 0; ++ } else { ++ *error_r = SQL_ERRSTR_NOT_CONNECTED; ++ return -1; + } ++#else ++ to = t_buffer_get(len * 2 + 1); ++ len = PQescapeString(to, string, len); + t_buffer_alloc(len + 1); +- return to; ++ *output_r = to; ++ return 0; ++#endif + } + + static void exec_callback(struct sql_result *_result, +Index: trixie/src/lib-sql/driver-sqlite.c +=================================================================== +--- trixie.orig/src/lib-sql/driver-sqlite.c ++++ trixie/src/lib-sql/driver-sqlite.c +@@ -221,15 +221,17 @@ static void driver_sqlite_deinit_v(struc + i_free(db); + } + +-static const char * ++static int + driver_sqlite_escape_string(struct sql_db *_db ATTR_UNUSED, +- const char *string) ++ const char *string, const char **output_r, ++ const char **error_r ATTR_UNUSED) + { + const size_t len = strlen(string) * 2 + 1; + char *escaped = t_malloc_no0(len); + if (sqlite3_snprintf(len, escaped, "%q", string) == NULL) + i_unreached(); +- return escaped; ++ *output_r = escaped; ++ return 0; + } + + static const char * +Index: trixie/src/lib-sql/driver-sqlpool.c +=================================================================== +--- trixie.orig/src/lib-sql/driver-sqlpool.c ++++ trixie/src/lib-sql/driver-sqlpool.c +@@ -573,8 +573,9 @@ static void driver_sqlpool_disconnect(st + driver_sqlpool_abort_requests(db); + } + +-static const char * +-driver_sqlpool_escape_string(struct sql_db *_db, const char *string) ++static int ++driver_sqlpool_escape_string(struct sql_db *_db, const char *string, ++ const char **output_r, const char **error_r) + { + struct sqlpool_db *db = (struct sqlpool_db *)_db; + const struct sqlpool_connection *conns; +@@ -584,11 +585,12 @@ driver_sqlpool_escape_string(struct sql_ + conns = array_get(&db->all_connections, &count); + for (i = 0; i < count; i++) { + if (SQL_DB_IS_READY(conns[i].db)) +- return sql_escape_string(conns[i].db, string); ++ return sql_escape_string(conns[i].db, string, ++ output_r, error_r); + } + /* no ready connections. just use the first one (we're guaranteed + to always have one) */ +- return sql_escape_string(conns[0].db, string); ++ return sql_escape_string(conns[0].db, string, output_r, error_r); + } + + static void driver_sqlpool_timeout(struct sqlpool_db *db) +Index: trixie/src/lib-sql/driver-test.c +=================================================================== +--- trixie.orig/src/lib-sql/driver-test.c ++++ trixie/src/lib-sql/driver-test.c +@@ -33,10 +33,12 @@ static int driver_test_sqlite_init(struc + static void driver_test_deinit(struct sql_db *_db); + static int driver_test_connect(struct sql_db *_db); + static void driver_test_disconnect(struct sql_db *_db); +-static const char * +-driver_test_mysql_escape_string(struct sql_db *_db, const char *string); +-static const char * +-driver_test_escape_string(struct sql_db *_db, const char *string); ++static int ++driver_test_mysql_escape_string(struct sql_db *_db, const char *string, ++ const char **output_r, const char **error_r); ++static int ++driver_test_escape_string(struct sql_db *_db, const char *string, ++ const char **output_r, const char **error_r); + static void driver_test_exec(struct sql_db *_db, const char *query); + static void driver_test_query(struct sql_db *_db, const char *query, + sql_query_callback_t *callback, void *context); +@@ -237,9 +239,10 @@ static int driver_test_connect(struct sq + static void driver_test_disconnect(struct sql_db *_db ATTR_UNUSED) + { } + +-static const char * ++static int + driver_test_mysql_escape_string(struct sql_db *_db ATTR_UNUSED, +- const char *string) ++ const char *string, const char **output_r, ++ const char **error_r ATTR_UNUSED) + { + string_t *esc = t_str_new(strlen(string)); + for(const char *ptr = string; *ptr != '\0'; ptr++) { +@@ -248,13 +251,17 @@ driver_test_mysql_escape_string(struct s + str_append_c(esc, '\\'); + str_append_c(esc, *ptr); + } +- return str_c(esc); ++ *output_r = str_c(esc); ++ return 0; + } + +-static const char * +-driver_test_escape_string(struct sql_db *_db ATTR_UNUSED, const char *string) ++static int ++driver_test_escape_string(struct sql_db *_db ATTR_UNUSED, const char *string, ++ const char **output_r, ++ const char **error_r ATTR_UNUSED) + { +- return string; ++ *output_r = string; ++ return 0; + } + + static void driver_test_exec(struct sql_db *_db, const char *query) +Index: trixie/src/lib-sql/sql-api-private.h +=================================================================== +--- trixie.orig/src/lib-sql/sql-api-private.h ++++ trixie/src/lib-sql/sql-api-private.h +@@ -81,7 +81,8 @@ struct sql_db_vfuncs { + + int (*connect)(struct sql_db *db); + void (*disconnect)(struct sql_db *db); +- const char *(*escape_string)(struct sql_db *db, const char *string); ++ int (*escape_string)(struct sql_db *db, const char *string, ++ const char **output_r, const char **error_r); + + void (*exec)(struct sql_db *db, const char *query); + /* Only implement this if the driver can really do asynchronous callbacks, +@@ -215,6 +216,7 @@ struct sql_statement { + pool_t pool; + const char *query_template; + ARRAY_TYPE(const_string) args; ++ ARRAY(bool) args_need_escaping; + + /* Tell the driver to not log this query with expanded values. + This works only for prepared statements. */ +@@ -252,6 +254,7 @@ struct sql_transaction_context { + /* commit() must use this query list if head is non-NULL. */ + struct sql_transaction_query *head, *tail; + ++ char *failed_error; + bool non_atomic; + }; + +@@ -277,7 +280,8 @@ inline static const char *sql_db_table_p + void sql_transaction_add_query(struct sql_transaction_context *ctx, pool_t pool, + const char *query, unsigned int *affected_rows); + const char *sql_statement_get_log_query(struct sql_statement *stmt); +-const char *sql_statement_get_query(struct sql_statement *stmt); ++int sql_statement_get_query(struct sql_statement *stmt, ++ const char **query_r, const char **error_r); + + void sql_connection_log_finished(struct sql_db *db); + struct event_passthrough * +Index: trixie/src/lib-sql/sql-api.c +=================================================================== +--- trixie.orig/src/lib-sql/sql-api.c ++++ trixie/src/lib-sql/sql-api.c +@@ -98,7 +98,6 @@ static const struct sql_result_vfuncs sq + .get_error = sql_result_error_get_error, + }; + +-static struct sql_result *sql_result_new_error(const char *error) ATTR_UNUSED; + static struct sql_result *sql_result_new_error(const char *error) + { + struct sql_result_error *result = i_new(struct sql_result_error, 1); +@@ -327,9 +326,10 @@ void sql_disconnect(struct sql_db *db) + db->v.disconnect(db); + } + +-const char *sql_escape_string(struct sql_db *db, const char *string) ++int sql_escape_string(struct sql_db *db, const char *string, ++ const char **output_r, const char **error_r) + { +- return db->v.escape_string(db, string); ++ return db->v.escape_string(db, string, output_r, error_r); + } + + const char *sql_escape_blob(struct sql_db *db, +@@ -391,19 +391,25 @@ default_sql_statement_init_prepared(stru + + const char *sql_statement_get_log_query(struct sql_statement *stmt) + { ++ const char *query, *error; + if (stmt->no_log_expanded_values) + return stmt->query_template; +- return sql_statement_get_query(stmt); ++ if (sql_statement_get_query(stmt, &query, &error) < 0) ++ return stmt->query_template; ++ return query; + } + +-const char *sql_statement_get_query(struct sql_statement *stmt) ++int sql_statement_get_query(struct sql_statement *stmt, ++ const char **query_r, const char **error_r) + { +- string_t *query = t_str_new(128); ++ string_t *query = str_new(default_pool, 128); + const char *const *args; +- unsigned int args_count, arg_pos = 0; ++ const bool *need_escaping_flags; ++ unsigned int args_count, need_escaping_count, arg_pos = 0; + const char *p0, *p1; + + args = array_get(&stmt->args, &args_count); ++ need_escaping_flags = array_get(&stmt->args_need_escaping, &need_escaping_count); + p0 = stmt->query_template; + while ((p1 = strchr(p0, '?')) != NULL) { + /* append until ? */ +@@ -413,7 +419,31 @@ const char *sql_statement_get_query(stru + i_panic("lib-sql: Missing bind for arg #%u in statement: %s", + arg_pos, stmt->query_template); + } +- str_append(query, args[arg_pos++]); ++ if (arg_pos < need_escaping_count && need_escaping_flags[arg_pos]) { ++ const char *escaped; ++ ++ /* Escape in a nested data stack frame so the ++ driver's temporary escape buffer is freed ++ immediately. The escaped value is appended to the ++ heap-allocated query before the frame is popped. */ ++ T_BEGIN { ++ if (sql_escape_string(stmt->db, args[arg_pos], ++ &escaped, error_r) < 0) ++ escaped = NULL; ++ else { ++ str_append_c(query, '\''); ++ str_append(query, escaped); ++ str_append_c(query, '\''); ++ } ++ } T_END_PASS_STR_IF(escaped == NULL, error_r); ++ if (escaped == NULL) { ++ str_free(&query); ++ return -1; ++ } ++ } else { ++ str_append(query, args[arg_pos]); ++ } ++ arg_pos++; + p0 = p1 + 1; + } + str_append(query, p0); +@@ -422,23 +452,37 @@ const char *sql_statement_get_query(stru + i_panic("lib-sql: Too many bind args (%u) for statement: %s", + args_count, stmt->query_template); + } +- return str_c(query); ++ *query_r = t_strdup(str_c(query)); ++ str_free(&query); ++ return 0; + } + + static void + default_sql_statement_query(struct sql_statement *stmt, + sql_query_callback_t *callback, void *context) + { +- sql_query(stmt->db, sql_statement_get_query(stmt), +- callback, context); ++ const char *query, *error; ++ if (sql_statement_get_query(stmt, &query, &error) < 0) { ++ sql_query_callback_delayed(stmt->db, ++ sql_result_new_error(error), ++ callback, context); ++ pool_unref(&stmt->pool); ++ return; ++ } ++ sql_query(stmt->db, query, callback, context); + pool_unref(&stmt->pool); + } + + static struct sql_result * + default_sql_statement_query_s(struct sql_statement *stmt) + { +- struct sql_result *result = +- sql_query_s(stmt->db, sql_statement_get_query(stmt)); ++ const char *query, *error; ++ if (sql_statement_get_query(stmt, &query, &error) < 0) { ++ struct sql_result *result = sql_result_new_error(error); ++ pool_unref(&stmt->pool); ++ return result; ++ } ++ struct sql_result *result = sql_query_s(stmt->db, query); + pool_unref(&stmt->pool); + return result; + } +@@ -447,8 +491,14 @@ static void default_sql_update_stmt(stru + struct sql_statement *stmt, + unsigned int *affected_rows) + { +- ctx->db->v.update(ctx, sql_statement_get_query(stmt), +- affected_rows); ++ const char *query, *error; ++ if (sql_statement_get_query(stmt, &query, &error) < 0) { ++ if (ctx->failed_error == NULL) ++ ctx->failed_error = i_strdup(error); ++ pool_unref(&stmt->pool); ++ return; ++ } ++ ctx->db->v.update(ctx, query, affected_rows); + pool_unref(&stmt->pool); + } + +@@ -487,6 +537,7 @@ sql_statement_init_fields(struct sql_sta + { + stmt->db = db; + p_array_init(&stmt->args, stmt->pool, 8); ++ p_array_init(&stmt->args_need_escaping, stmt->pool, 8); + } + + struct sql_statement * +@@ -545,10 +596,10 @@ void sql_statement_set_no_log_expanded_v + void sql_statement_bind_str(struct sql_statement *stmt, + unsigned int column_idx, const char *value) + { +- const char *escaped_value = +- p_strdup_printf(stmt->pool, "'%s'", +- sql_escape_string(stmt->db, value)); +- array_idx_set(&stmt->args, column_idx, &escaped_value); ++ const char *value_dup = p_strdup(stmt->pool, value); ++ array_idx_set(&stmt->args, column_idx, &value_dup); ++ bool needs_escaping = TRUE; ++ array_idx_set(&stmt->args_need_escaping, column_idx, &needs_escaping); + + if (stmt->db->v.statement_bind_str != NULL) + stmt->db->v.statement_bind_str(stmt, column_idx, value); +@@ -908,6 +959,13 @@ void sql_transaction_commit(struct sql_t + struct sql_db *db = ctx->db; + *_ctx = NULL; + ++ if (ctx->failed_error != NULL) { ++ sql_commit_schedule_delayed(db, ctx->failed_error, callback, context); ++ i_free(ctx->failed_error); ++ ctx->db->v.transaction_rollback(ctx); ++ return; ++ } ++ + if (ctx->db->v.transaction_commit != NULL) { + ctx->db->v.transaction_commit(ctx, callback, context); + return; +@@ -924,6 +982,12 @@ int sql_transaction_commit_s(struct sql_ + struct sql_transaction_context *ctx = *_ctx; + + *_ctx = NULL; ++ if (ctx->failed_error != NULL) { ++ *error_r = t_strdup(ctx->failed_error); ++ i_free(ctx->failed_error); ++ ctx->db->v.transaction_rollback(ctx); ++ return -1; ++ } + return ctx->db->v.transaction_commit_s(ctx, error_r); + } + +@@ -932,6 +996,7 @@ void sql_transaction_rollback(struct sql + struct sql_transaction_context *ctx = *_ctx; + + *_ctx = NULL; ++ i_free(ctx->failed_error); + ctx->db->v.transaction_rollback(ctx); + } + +Index: trixie/src/lib-sql/sql-api.h +=================================================================== +--- trixie.orig/src/lib-sql/sql-api.h ++++ trixie/src/lib-sql/sql-api.h +@@ -114,7 +114,8 @@ int sql_connect(struct sql_db *db); + void sql_disconnect(struct sql_db *db); + + /* Escape the given string if needed and return it. */ +-const char *sql_escape_string(struct sql_db *db, const char *string); ++int sql_escape_string(struct sql_db *db, const char *string, ++ const char **output_r, const char **error_r); + /* Escape the given data as a string. */ + const char *sql_escape_blob(struct sql_db *db, + const unsigned char *data, size_t size); diff --git a/patches/0005-submission-login-submission-proxy-Recognize-421-4.7..patch b/patches/0005-submission-login-submission-proxy-Recognize-421-4.7..patch new file mode 100644 index 0000000..4a418d1 --- /dev/null +++ b/patches/0005-submission-login-submission-proxy-Recognize-421-4.7..patch @@ -0,0 +1,50 @@ +From 8157e395df6b19891cea807fbb1f59e9f8b5a30a Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 7 May 2026 10:58:24 +0000 +Subject: [PATCH 5/5] submission-login: submission-proxy - Recognize 421 4.7.0 + as connection-limit reply + +When the backend signals that the user's connection limit has been +reached, do not classify the response as a generic temporary +authentication failure (which causes the proxy to reconnect). Map it +instead to LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED, which +suppresses retries and reports the failure as proxy_dest_connection_limit +in the login_aborted event/log line. This mirrors what imap-login does +for the [LIMIT] response code and pop3-login for [IN-USE]. + +Other 421 replies (typically server shutdown / fatal error) are mapped +to LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED rather than AUTH_TEMPFAIL: 421 +means "closing transmission channel" (RFC 5321 Section 4.2.1), so a +reconnect on the same destination is unlikely to help and should not +happen automatically. +--- + src/submission-login/submission-proxy.c | 16 +++++++++++++++- + 1 file changed, 15 insertions(+), 1 deletion(-) + +Index: dovecot/src/submission-login/submission-proxy.c +=================================================================== +--- dovecot.orig/src/submission-login/submission-proxy.c ++++ dovecot/src/submission-login/submission-proxy.c +@@ -694,7 +694,21 @@ int submission_proxy_parse_line(struct c + be using only Dovecot as their backend :) */ + enum login_proxy_failure_type failure_type = + LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED; +- if ((status / 100) == 4) ++ if (status == 421) { ++ /* The backend won't accept further AUTH attempts on this ++ connection. If it signaled 4.7.0, the user's connection ++ limit (mail_max_userip_connections) was reached; report ++ that specifically. Otherwise treat as a generic auth ++ reply (e.g. backend shutting down). The reply was ++ prepared from the backend's 421 line; forward it to the ++ client and detach the AUTH command. */ ++ failure_type = (null_strcmp(enh_code, "4.7.0") == 0) ? ++ LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED : ++ LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED; ++ i_assert(subm_client->proxy_reply != NULL); ++ smtp_server_reply_submit(subm_client->proxy_reply); ++ subm_client->pending_auth = NULL; ++ } else if ((status / 100) == 4) + failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL; + else if (!submission_proxy_handle_redirect( + client, status, enh_code, text, &failure_type, &text)) { diff --git a/patches/0006-auth-passdb_sql-connect-before-expanding-query-varia.patch b/patches/0006-auth-passdb_sql-connect-before-expanding-query-varia.patch new file mode 100644 index 0000000..b7f0d8b --- /dev/null +++ b/patches/0006-auth-passdb_sql-connect-before-expanding-query-varia.patch @@ -0,0 +1,35 @@ +From abb51a4040fa21314c54a675cf35c4103e92da9c Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Fri, 29 May 2026 09:28:40 +0300 +Subject: [PATCH 6/6] auth: passdb_sql: connect before expanding query + variables + +sql_lookup_pass() calls settings_get_params() with an SQL escape func that +requires a live connection. If the DB is disconnected, the error propagated +as "Failed to parse configuration" instead of a DB connectivity error. + +Call sql_connect() first; fail with "Not connected to database" directly +if it returns -1. +--- + src/auth/passdb-sql.c | 8 ++++++++ + 1 file changed, 8 insertions(+) + +Index: trixie/src/auth/passdb-sql.c +=================================================================== +--- trixie.orig/src/auth/passdb-sql.c ++++ trixie/src/auth/passdb-sql.c +@@ -183,6 +183,14 @@ static void sql_lookup_pass(struct passd + const struct passdb_sql_settings *set; + const char *error; + ++ if (sql_connect(module->db) < 0) { ++ e_error(authdb_event(sql_request->auth_request), ++ "Not connected to database"); ++ sql_request->callback.verify_plain( ++ PASSDB_RESULT_INTERNAL_FAILURE, ++ sql_request->auth_request); ++ return; ++ } + const struct settings_get_params params = { + .escape_func = passdb_sql_escape, + .escape_context = module->db, diff --git a/patches/0006-imap-Reset-DEFLATE-dictionary-after-every-tagged-rep.patch b/patches/0006-imap-Reset-DEFLATE-dictionary-after-every-tagged-rep.patch new file mode 100644 index 0000000..46310a4 --- /dev/null +++ b/patches/0006-imap-Reset-DEFLATE-dictionary-after-every-tagged-rep.patch @@ -0,0 +1,61 @@ +From 5ea530ea3677f5fe9d0a7040af87623f3f7af7ce Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 16 Apr 2026 17:38:53 +0200 +Subject: [PATCH 6/6] imap: Reset DEFLATE dictionary after every tagged reply + +Prevents CRIME-style cross-command compression oracle attacks +(CVE-class: compression side-channel). Without this fix an observer +who can inject chosen plaintext into one IMAP command's response can +measure the compressed size of a subsequent command's response and +determine whether the secret content matches the injected plaintext. + +After each tagged response line is sent, the DEFLATE compression +dictionary is reset via Z_FULL_FLUSH so that the compression history +from one command cannot influence the compressed size of the next. + +For direct compression (imap_compress_on_proxy=no) the reset is applied +to the local ostream. For proxy-mode compression +(imap_compress_on_proxy=yes) a "dict_reset" command is sent over the +multiplex side channel to the imap-login process. +--- + src/imap/imap-client.c | 17 +++++++++++++++++ + 1 file changed, 17 insertions(+) + +diff --git a/src/imap/imap-client.c b/src/imap/imap-client.c +index 4c8115f9a0..c44a4d810a 100644 +--- a/src/imap/imap-client.c ++++ b/src/imap/imap-client.c +@@ -12,6 +12,7 @@ + #include "istream-concat.h" + #include "ostream.h" + #include "ostream-multiplex.h" ++#include "ostream-zlib.h" + #include "time-util.h" + #include "settings.h" + #include "master-service.h" +@@ -712,6 +713,22 @@ client_default_send_tagline(struct client_command_context *cmd, const char *data + } T_END; + + client->last_output = ioloop_time; ++ ++ /* Reset the DEFLATE compression dictionary after every tagged reply so ++ that cross-command compression correlation attacks (CRIME-style) are ++ not possible. For proxy-mode compression the reset is forwarded to ++ the imap-login process via the side channel; for direct compression ++ it is applied to the local ostream immediately. */ ++ if (client->compress_handler != NULL) { ++ if (client->multiplex_output != NULL && ++ client->set->imap_compress_on_proxy) { ++ i_assert(client->side_channel_output != NULL); ++ o_stream_nsend_str(client->side_channel_output, ++ "dict_reset\n"); ++ } else { ++ o_stream_deflate_reset_dict(client->output); ++ } ++ } + } + + static int +-- +2.47.3 + diff --git a/patches/0006-lib-index-Rename-crc32-variables-parameters-to-hash-.patch b/patches/0006-lib-index-Rename-crc32-variables-parameters-to-hash-.patch new file mode 100644 index 0000000..d561ab7 --- /dev/null +++ b/patches/0006-lib-index-Rename-crc32-variables-parameters-to-hash-.patch @@ -0,0 +1,366 @@ +From ed3c3d25b3ac571925f19b9b4d0dd8e8a718227e Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 13 Apr 2026 12:52:15 +0200 +Subject: [PATCH 06/12] lib-index: Rename crc32 variables/parameters to hash in + strmap + +Pure rename - no functional change. The stored 32-bit value is still +produced by crc32_str_nonzero(); only the identifiers change to prepare +for swapping the hash algorithm in the next commit. + +recs_crc32 -> recs_hash, hash_key.crc32 -> hash_key.hash, +crc32_r -> hash_r, local crc32 -> hash, *crc32 -> *hashes. +--- + src/lib-index/mail-index-strmap.c | 84 +++++++++++++++---------------- + 1 file changed, 42 insertions(+), 42 deletions(-) + +diff --git a/src/lib-index/mail-index-strmap.c b/src/lib-index/mail-index-strmap.c +index 9ce2de9103..254080dbb7 100644 +--- a/src/lib-index/mail-index-strmap.c ++++ b/src/lib-index/mail-index-strmap.c +@@ -31,7 +31,7 @@ struct mail_index_strmap_view { + struct mail_index_view *view; + + ARRAY_TYPE(mail_index_strmap_rec) recs; +- ARRAY(uint32_t) recs_crc32; ++ ARRAY(uint32_t) recs_hash; + struct hash2_table *hash; + + mail_index_strmap_key_cmp_t *key_compare; +@@ -74,7 +74,7 @@ struct mail_index_strmap_view_sync { + + struct mail_index_strmap_hash_key { + const char *str; +- uint32_t crc32; ++ uint32_t hash; + }; + + /* renumber the string indexes when highest string idx becomes larger than +@@ -113,7 +113,7 @@ mail_index_strmap_init(struct mail_index *index, const char *suffix) + + static bool + mail_index_strmap_read_rec_next(struct mail_index_strmap_read_context *ctx, +- uint32_t *crc32_r); ++ uint32_t *hash_r); + + static void + mail_index_strmap_set_syscall_error(struct mail_index_strmap *strmap, +@@ -162,7 +162,7 @@ static unsigned int mail_index_strmap_hash_key(const void *_key) + { + const struct mail_index_strmap_hash_key *key = _key; + +- return key->crc32; ++ return key->hash; + } + + static bool +@@ -197,7 +197,7 @@ mail_index_strmap_view_open(struct mail_index_strmap *strmap, + view->next_str_idx = 1; + + i_array_init(&view->recs, 64); +- i_array_init(&view->recs_crc32, 64); ++ i_array_init(&view->recs_hash, 64); + view->hash = hash2_create(0, sizeof(struct mail_index_strmap_rec), + mail_index_strmap_hash_key, + mail_index_strmap_hash_cmp, view); +@@ -212,7 +212,7 @@ void mail_index_strmap_view_close(struct mail_index_strmap_view **_view) + + *_view = NULL; + array_free(&view->recs); +- array_free(&view->recs_crc32); ++ array_free(&view->recs_hash); + hash2_destroy(&view->hash); + i_free(view); + } +@@ -226,7 +226,7 @@ static void mail_index_strmap_view_reset(struct mail_index_strmap_view *view) + { + view->remap_cb(NULL, 0, 0, view->cb_context); + array_clear(&view->recs); +- array_clear(&view->recs_crc32); ++ array_clear(&view->recs_hash); + hash2_clear(view->hash); + + view->last_added_uid = 0; +@@ -410,13 +410,13 @@ mail_index_strmap_uid_exists(struct mail_index_strmap_read_context *ctx, + + static int + mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx, +- uint32_t *crc32_r) ++ uint32_t *hash_r) + { + size_t size; + uint32_t n, i, count, str_idx; + int ret; + +- /* *count *count ++ /* *count *count + where + n = 0 -> count=1 (only Message-ID:) + n = 1 -> count=2 (Message-ID: + In-Reply-To:) +@@ -426,11 +426,11 @@ mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx, + return -1; + count = n < 2 ? n + 1 : n; + /* check that rec_size fits */ +- if (UINT_MAX / (sizeof(ctx->rec.str_idx) + sizeof(*crc32_r)) < count) ++ if (UINT_MAX / (sizeof(ctx->rec.str_idx) + sizeof(*hash_r)) < count) + return -1; + ctx->view->total_ref_count += count; + +- ctx->rec_size = count * (sizeof(ctx->rec.str_idx) + sizeof(*crc32_r)); ++ ctx->rec_size = count * (sizeof(ctx->rec.str_idx) + sizeof(*hash_r)); + ret = mail_index_strmap_uid_exists(ctx, ctx->rec.uid); + if (ret < 0) + return -1; +@@ -454,10 +454,10 @@ mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx, + /* everything exists. save it. FIXME: these ref_index values + are thread index specific, perhaps something more generic + should be used some day */ +- ctx->end = ctx->data + count * sizeof(*crc32_r); ++ ctx->end = ctx->data + count * sizeof(*hash_r); + + ctx->next_ref_index = 0; +- if (!mail_index_strmap_read_rec_next(ctx, crc32_r)) ++ if (!mail_index_strmap_read_rec_next(ctx, hash_r)) + i_unreached(); + ctx->next_ref_index = n == 1 ? 1 : 2; + return 1; +@@ -465,7 +465,7 @@ mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx, + + static bool + mail_index_strmap_read_rec_next(struct mail_index_strmap_read_context *ctx, +- uint32_t *crc32_r) ++ uint32_t *hash_r) + { + if (ctx->data == ctx->end) { + i_stream_skip(ctx->view->strmap->input, ctx->rec_size); +@@ -478,7 +478,7 @@ mail_index_strmap_read_rec_next(struct mail_index_strmap_read_context *ctx, + + /* read the record contents */ + memcpy(&ctx->rec.str_idx, ctx->str_idx_base, sizeof(ctx->rec.str_idx)); +- memcpy(crc32_r, ctx->data, sizeof(*crc32_r)); ++ memcpy(hash_r, ctx->data, sizeof(*hash_r)); + + ctx->rec.ref_index = ctx->next_ref_index++; + +@@ -486,7 +486,7 @@ mail_index_strmap_read_rec_next(struct mail_index_strmap_read_context *ctx, + ctx->highest_str_idx = ctx->rec.str_idx; + + /* get to the next record */ +- ctx->data += sizeof(*crc32_r); ++ ctx->data += sizeof(*hash_r); + ctx->str_idx_base += sizeof(ctx->rec.str_idx); + return TRUE; + } +@@ -547,12 +547,12 @@ strmap_read_block_init(struct mail_index_strmap_view *view, + + static int + strmap_read_block_next(struct mail_index_strmap_read_context *ctx, +- uint32_t *crc32_r) ++ uint32_t *hash_r) + { + uint32_t uid_diff; + int ret; + +- if (mail_index_strmap_read_rec_next(ctx, crc32_r)) ++ if (mail_index_strmap_read_rec_next(ctx, hash_r)) + return 1; + + /* get next UID */ +@@ -565,7 +565,7 @@ strmap_read_block_next(struct mail_index_strmap_read_context *ctx, + return -1; + + ctx->rec.uid += uid_diff; +- ret = mail_index_strmap_read_rec_first(ctx, crc32_r); ++ ret = mail_index_strmap_read_rec_first(ctx, hash_r); + } while (ret == 0); + return ret; + } +@@ -635,12 +635,12 @@ strmap_view_sync_handle_conflict(struct mail_index_strmap_read_context *ctx, + + static int + strmap_view_sync_block_check_conflicts(struct mail_index_strmap_read_context *ctx, +- uint32_t crc32) ++ uint32_t hash) + { + struct mail_index_strmap_rec *hash_rec; + struct hash2_iter iter; + +- if (crc32 == 0) { ++ if (hash == 0) { + /* unique string - there are no conflicts */ + return 0; + } +@@ -657,9 +657,9 @@ strmap_view_sync_block_check_conflicts(struct mail_index_strmap_read_context *ct + strmap index until X has been expunged. */ + i_zero(&iter); + while ((hash_rec = hash2_iterate(ctx->view->hash, +- crc32, &iter)) != NULL && ++ hash, &iter)) != NULL && + hash_rec->str_idx != ctx->rec.str_idx) { +- /* CRC32 matches, but string index doesn't */ ++ /* hash matches, but string index doesn't */ + if (!strmap_view_sync_handle_conflict(ctx, hash_rec, &iter)) { + ctx->lost_expunged_uid = hash_rec->uid; + return -1; +@@ -672,10 +672,10 @@ static int + mail_index_strmap_view_sync_block(struct mail_index_strmap_read_context *ctx) + { + struct mail_index_strmap_rec *hash_rec; +- uint32_t crc32, prev_uid = 0; ++ uint32_t hash, prev_uid = 0; + int ret; + +- while ((ret = strmap_read_block_next(ctx, &crc32)) > 0) { ++ while ((ret = strmap_read_block_next(ctx, &hash)) > 0) { + if (ctx->rec.uid <= ctx->view->last_added_uid) { + if (ctx->rec.uid < ctx->view->last_added_uid || + prev_uid != ctx->rec.uid) { +@@ -685,7 +685,7 @@ mail_index_strmap_view_sync_block(struct mail_index_strmap_read_context *ctx) + } + prev_uid = ctx->rec.uid; + +- if (strmap_view_sync_block_check_conflicts(ctx, crc32) < 0) { ++ if (strmap_view_sync_block_check_conflicts(ctx, hash) < 0) { + ret = -1; + break; + } +@@ -693,10 +693,10 @@ mail_index_strmap_view_sync_block(struct mail_index_strmap_read_context *ctx) + + /* add the record to records array */ + array_push_back(&ctx->view->recs, &ctx->rec); +- array_push_back(&ctx->view->recs_crc32, &crc32); ++ array_push_back(&ctx->view->recs_hash, &hash); + + /* add a separate copy of the record to hash */ +- hash_rec = hash2_insert_hash(ctx->view->hash, crc32); ++ hash_rec = hash2_insert_hash(ctx->view->hash, hash); + memcpy(hash_rec, &ctx->rec, sizeof(*hash_rec)); + } + return strmap_read_block_deinit(ctx, ret, TRUE); +@@ -764,7 +764,7 @@ void mail_index_strmap_view_sync_add(struct mail_index_strmap_view_sync *sync, + ref_index > view->last_ref_index)); + + hash_key.str = key; +- hash_key.crc32 = crc32_str_nonzero(key); ++ hash_key.hash = crc32_str_nonzero(key); + + old_rec = hash2_lookup(view->hash, &hash_key); + if (old_rec != NULL) { +@@ -781,7 +781,7 @@ void mail_index_strmap_view_sync_add(struct mail_index_strmap_view_sync *sync, + rec->ref_index = ref_index; + rec->str_idx = str_idx; + array_push_back(&view->recs, rec); +- array_push_back(&view->recs_crc32, &hash_key.crc32); ++ array_push_back(&view->recs_hash, &hash_key.hash); + + view->last_added_uid = uid; + view->last_ref_index = ref_index; +@@ -802,7 +802,7 @@ void mail_index_strmap_view_sync_add_unique(struct mail_index_strmap_view_sync * + rec.ref_index = ref_index; + rec.str_idx = view->next_str_idx++; + array_push_back(&view->recs, &rec); +- array_append_zero(&view->recs_crc32); ++ array_append_zero(&view->recs_hash); + + view->last_added_uid = uid; + view->last_ref_index = ref_index; +@@ -820,7 +820,7 @@ static void mail_index_strmap_view_renumber(struct mail_index_strmap_view *view) + { + struct mail_index_strmap_read_context ctx; + struct mail_index_strmap_rec *recs, *hash_rec; +- uint32_t prev_uid, str_idx, *recs_crc32, *renumber_map; ++ uint32_t prev_uid, str_idx, *recs_hash, *renumber_map; + unsigned int i, dest, count, count2; + int ret; + +@@ -833,7 +833,7 @@ static void mail_index_strmap_view_renumber(struct mail_index_strmap_view *view) + renumber_map = i_new(uint32_t, view->next_str_idx); + str_idx = 0; prev_uid = 0; + recs = array_get_modifiable(&view->recs, &count); +- recs_crc32 = array_get_modifiable(&view->recs_crc32, &count2); ++ recs_hash = array_get_modifiable(&view->recs_hash, &count2); + i_assert(count == count2); + + for (i = dest = 0; i < count; ) { +@@ -856,13 +856,13 @@ static void mail_index_strmap_view_renumber(struct mail_index_strmap_view *view) + renumber_map[recs[i].str_idx] = ++str_idx; + if (i != dest) { + recs[dest] = recs[i]; +- recs_crc32[dest] = recs_crc32[i]; ++ recs_hash[dest] = recs_hash[i]; + } + i++; dest++; + } + i_assert(renumber_map[0] == 0); + array_delete(&view->recs, dest, i-dest); +- array_delete(&view->recs_crc32, dest, i-dest); ++ array_delete(&view->recs_hash, dest, i-dest); + mail_index_strmap_zero_terminate(view); + + /* notify caller of the renumbering */ +@@ -875,7 +875,7 @@ static void mail_index_strmap_view_renumber(struct mail_index_strmap_view *view) + hash2_clear(view->hash); + for (i = 0; i < count; i++) { + recs[i].str_idx = renumber_map[recs[i].str_idx]; +- hash_rec = hash2_insert_hash(view->hash, recs_crc32[i]); ++ hash_rec = hash2_insert_hash(view->hash, recs_hash[i]); + memcpy(hash_rec, &recs[i], sizeof(*hash_rec)); + } + +@@ -889,7 +889,7 @@ static void mail_index_strmap_write_block(struct mail_index_strmap_view *view, + unsigned int i, uint32_t base_uid) + { + const struct mail_index_strmap_rec *recs; +- const uint32_t *crc32; ++ const uint32_t *hashes; + unsigned int j, n, count, count2, uid_rec_count; + uint32_t block_size; + uint8_t *p, packed[MAIL_INDEX_PACK_MAX_SIZE*2]; +@@ -902,7 +902,7 @@ static void mail_index_strmap_write_block(struct mail_index_strmap_view *view, + + /* write records */ + recs = array_get(&view->recs, &count); +- crc32 = array_get(&view->recs_crc32, &count2); ++ hashes = array_get(&view->recs_hash, &count2); + i_assert(count == count2); + while (i < count) { + /* @UNSAFE: */ +@@ -919,7 +919,7 @@ static void mail_index_strmap_write_block(struct mail_index_strmap_view *view, + } + view->total_ref_count += uid_rec_count; + +- /* *count *count - ++ /* *count *count - + FIXME: thread index specific code */ + i_assert(recs[i].ref_index == 0); + if (uid_rec_count == 1) { +@@ -938,7 +938,7 @@ static void mail_index_strmap_write_block(struct mail_index_strmap_view *view, + mail_index_pack_num(&p, n); + o_stream_nsend(output, packed, p-packed); + for (j = 0; j < uid_rec_count; j++) +- o_stream_nsend(output, &crc32[i+j], sizeof(crc32[i+j])); ++ o_stream_nsend(output, &hashes[i+j], sizeof(hashes[i+j])); + for (j = 0; j < uid_rec_count; j++) { + i_assert(j < 2 || recs[i+j].ref_index == j+1); + o_stream_nsend(output, &recs[i+j].str_idx, +@@ -1095,7 +1095,7 @@ mail_index_strmap_write_append(struct mail_index_strmap_view *view) + const struct mail_index_strmap_rec *old_recs; + unsigned int i, old_count; + struct ostream *output; +- uint32_t crc32, next_uid; ++ uint32_t hash, next_uid; + bool full_block; + int ret; + +@@ -1134,7 +1134,7 @@ mail_index_strmap_write_append(struct mail_index_strmap_view *view) + full_block = TRUE; ret = 0; + while (i < old_count && + (ret = strmap_read_block_init(view, &ctx)) > 0) { +- while ((ret = strmap_read_block_next(&ctx, &crc32)) > 0) { ++ while ((ret = strmap_read_block_next(&ctx, &hash)) > 0) { + if (ctx.rec.uid != old_recs[i].uid || + ctx.rec.str_idx != old_recs[i].str_idx) { + /* mismatch */ +-- +2.47.3 + diff --git a/patches/0006-lib-sieve-Log-a-warning-when-cumulative-CPU-exceeds-.patch b/patches/0006-lib-sieve-Log-a-warning-when-cumulative-CPU-exceeds-.patch new file mode 100644 index 0000000..43646f8 --- /dev/null +++ b/patches/0006-lib-sieve-Log-a-warning-when-cumulative-CPU-exceeds-.patch @@ -0,0 +1,55 @@ +From 025233779751d7ba8cff7c615f8e229b604d92a5 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 3 Jun 2026 12:25:38 +0000 +Subject: [PATCH 6/7] lib-sieve: Log a warning when cumulative CPU exceeds the + limit + +sieve_binary_record_resource_usage() returned FALSE when the script's +cumulative CPU time crossed sieve_max_cpu_time, but the only log line +on that path was a debug-level "Updated cumulative resource usage" +emitted on every recording, with no signal at the actual disable +transition. Operators had to infer the disable from later "Failed to +open script ... (cumulative resource limit exceeded)" errors on +subsequent deliveries. + +Emit a warning at the transition point, including the cumulative CPU +usage that triggered it. +--- + src/lib-sieve/sieve-binary.c | 17 ++++++++++++++++- + 1 file changed, 16 insertions(+), 1 deletion(-) + +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary.c +@@ -248,6 +248,7 @@ bool sieve_binary_record_resource_usage( + struct sieve_binary *sbin, const struct sieve_resource_usage *rusage) + { + struct sieve_resource_usage rusage_total; ++ struct sieve_script *script; + + if (sbin == NULL) + return TRUE; +@@ -262,7 +263,21 @@ bool sieve_binary_record_resource_usage( + e_debug(sbin->event, "Updated cumulative resource usage: %s", + sieve_resource_usage_get_summary(&rusage_total)); + +- return sieve_binary_check_resource_usage(sbin); ++ if (!sieve_resource_usage_is_excessive(sbin->svinst, &rusage_total)) ++ return TRUE; ++ ++ /* Cumulative resource usage is only tracked for personal scripts; ++ all callers gate this on the user's own script. */ ++ script = sbin->script; ++ i_assert(script != NULL); ++ i_assert(sieve_storage_is_personal(script->storage)); ++ ++ e_warning(sbin->event, ++ "Personal sieve processing disabled for script '%s': " ++ "cumulative resource usage limit exceeded (%s)", ++ sieve_script_label(script), ++ sieve_resource_usage_get_summary(&rusage_total)); ++ return FALSE; + } + + void sieve_binary_set_resource_usage(struct sieve_binary *sbin, diff --git a/patches/0006-lib-storage-mbox-Explicitly-disable-the-header-block.patch b/patches/0006-lib-storage-mbox-Explicitly-disable-the-header-block.patch new file mode 100644 index 0000000..4c517be --- /dev/null +++ b/patches/0006-lib-storage-mbox-Explicitly-disable-the-header-block.patch @@ -0,0 +1,50 @@ +From 89ed3baf87b789f55d7513252ef2dead41cf4105 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sun, 19 Apr 2026 15:57:08 +0300 +Subject: [PATCH 06/14] lib-storage/mbox: Explicitly disable the header block + size limit + +mbox_sync_parse_next_mail() appends each header's raw bytes into +ctx->header and writes them back when rewriting the mbox file. The +default MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE limit (10 MB) must not +apply here: any truncation would corrupt the mbox on rewrite. +Similarly, mbox_sync_parse_match_mail() feeds full raw header bytes +into the MD5 verifier. + +Today the per-chunk hdr->value delivered by message_parse_header_next() +is not clamped cumulatively, so the limit only bites on full_value and +unknown headers pass through intact. That is about to change - a +subsequent commit will clamp hdr->value cumulatively. Explicitly +setting SIZE_MAX here locks in the intent and prevents a regression. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + src/lib-storage/index/mbox/mbox-sync-parse.c | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/src/lib-storage/index/mbox/mbox-sync-parse.c b/src/lib-storage/index/mbox/mbox-sync-parse.c +index 90f032a017..660e9b63d0 100644 +--- a/src/lib-storage/index/mbox/mbox-sync-parse.c ++++ b/src/lib-storage/index/mbox/mbox-sync-parse.c +@@ -479,6 +479,9 @@ int mbox_sync_parse_next_mail(struct istream *input, + + line_start_pos = 0; + hdr_ctx = message_parse_header_init(input, NULL, 0); ++ /* Rewriting the mbox requires the full raw header bytes, so disable ++ the default per-header block size limit. */ ++ message_parse_header_set_limit(hdr_ctx, SIZE_MAX); + while ((ret = message_parse_header_next(hdr_ctx, &hdr)) > 0) { + if (hdr->eoh) { + ctx->have_eoh = TRUE; +@@ -576,6 +579,8 @@ bool mbox_sync_parse_match_mail(struct mbox_mailbox *mbox, + mbox_md5_ctx = mbox->md5_v.init(); + + hdr_ctx = message_parse_header_init(mbox->mbox_stream, NULL, 0); ++ /* MD5 matching must see the full raw header bytes. */ ++ message_parse_header_set_limit(hdr_ctx, SIZE_MAX); + while ((ret = message_parse_header_next(hdr_ctx, &hdr)) > 0) { + if (hdr->eoh) + break; +-- +2.47.3 + diff --git a/patches/0007-lib-index-Add-keyed-xxh64-strmap-format-v2-gated-by-.patch b/patches/0007-lib-index-Add-keyed-xxh64-strmap-format-v2-gated-by-.patch new file mode 100644 index 0000000..76da814 --- /dev/null +++ b/patches/0007-lib-index-Add-keyed-xxh64-strmap-format-v2-gated-by-.patch @@ -0,0 +1,364 @@ +From ef45dd7c36562ef07cccf6cc4ad245e0041af456 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 30 Apr 2026 12:11:57 +0000 +Subject: [PATCH 07/12] lib-index: Add keyed xxh64 strmap format v2, gated by + config version + +The on-disk hash stored in the strmap file is now optionally a keyed +xxh64_to_32() with a per-file random 64-bit IV stored in the file +header. This replaces the previous plain crc32_str_nonzero() output +and hardens the strmap against deliberately-collided message-id +hashes. The v1 (crc32) format remains fully readable and writable +so older configurations keep working without forced rebuilds. + +On-disk: + - v1 header is 8 bytes: version, 3 unused, uid_validity (unchanged). + - v2 header is 16 bytes: version, compat_flags, 2 unused, uid_validity, + hash_iv. The first 8 bytes of v2 align with v1 byte-for-byte, so + the open path reads 8 bytes, dispatches on version, and reads the + trailing 8 bytes only for v2. + +Runtime: + - strmap->enable_xxh64 selects the format used when creating a new + file (or recreating one whose uid_validity has changed). + - view->format_version follows the on-disk file when one exists; for + fresh files it follows the strmap-wide preference. + - Renumber recreate_write() preserves view->format_version, so we + never silently migrate an existing file across the threshold while + it is still in use - the migration only happens at open time. + - When enable_xxh64 is TRUE and the on-disk file is v1, open treats + it as a version mismatch: the file is unlinked and the next sync's + recreate_write() produces a v2 file. Below the threshold v1 stays + v1 indefinitely. + - strmap_hash_str() dispatches per-view, so v1 files keep using + crc32 hashes and v2 files use keyed xxh64 even in mixed setups. +--- + src/lib-index/mail-index-strmap.c | 106 ++++++++++++++++++++++++--- + src/lib-index/mail-index-strmap.h | 17 ++++- + src/lib-master/Makefile.am | 1 + + src/lib-master/storage-version.h | 22 ++++++ + src/lib-storage/index/index-thread.c | 12 ++- + 5 files changed, 143 insertions(+), 15 deletions(-) + create mode 100644 src/lib-master/storage-version.h + +Index: trixie/src/lib-index/mail-index-strmap.c +=================================================================== +--- trixie.orig/src/lib-index/mail-index-strmap.c ++++ trixie/src/lib-index/mail-index-strmap.c +@@ -8,6 +8,8 @@ + #include "file-lock.h" + #include "file-dotlock.h" + #include "crc32.h" ++#include "randgen.h" ++#include "xxh64.h" + #include "safe-mkstemp.h" + #include "str.h" + #include "mail-index-private.h" +@@ -24,6 +26,12 @@ struct mail_index_strmap { + struct file_lock *file_lock; + struct dotlock *dotlock; + struct dotlock_settings dotlock_settings; ++ ++ /* If TRUE, new files are created in v2 format (xxh64-keyed hashes). ++ If FALSE, new files are created in v1 format (crc32). Existing ++ files of either version are always read; on rebuild they are ++ recreated in this preferred format. */ ++ bool enable_xxh64; + }; + + struct mail_index_strmap_view { +@@ -34,6 +42,14 @@ struct mail_index_strmap_view { + ARRAY(uint32_t) recs_hash; + struct hash2_table *hash; + ++ /* On-disk format version that view's hash values are computed ++ against. v1 = crc32_str_nonzero, v2 = keyed xxh64. Set from the ++ file header when opening an existing file; otherwise from ++ strmap->enable_xxh64. */ ++ uint8_t format_version; ++ /* Per-file random IV mixed into the xxh64 hash. Unused for v1. */ ++ uint64_t hash_iv; ++ + mail_index_strmap_key_cmp_t *key_compare; + mail_index_strmap_rec_cmp_t *rec_compare; + mail_index_strmap_remap_t *remap_cb; +@@ -92,7 +108,8 @@ static const struct dotlock_settings def + }; + + struct mail_index_strmap * +-mail_index_strmap_init(struct mail_index *index, const char *suffix) ++mail_index_strmap_init(struct mail_index *index, const char *suffix, ++ bool enable_xxh64) + { + struct mail_index_strmap *strmap; + +@@ -102,6 +119,7 @@ mail_index_strmap_init(struct mail_index + strmap->index = index; + strmap->path = i_strconcat(index->filepath, suffix, NULL); + strmap->fd = -1; ++ strmap->enable_xxh64 = enable_xxh64; + + strmap->dotlock_settings = default_dotlock_settings; + strmap->dotlock_settings.use_excl_lock = +@@ -198,6 +216,9 @@ mail_index_strmap_view_open(struct mail_ + + i_array_init(&view->recs, 64); + i_array_init(&view->recs_hash, 64); ++ view->format_version = strmap->enable_xxh64 ? ++ MAIL_INDEX_STRMAP_VERSION_V2 : MAIL_INDEX_STRMAP_VERSION_V1; ++ random_fill(&view->hash_iv, sizeof(view->hash_iv)); + view->hash = hash2_create(0, sizeof(struct mail_index_strmap_rec), + mail_index_strmap_hash_key, + mail_index_strmap_hash_cmp, view); +@@ -250,7 +271,7 @@ static int mail_index_strmap_open(struct + const struct mail_index_header *idx_hdr; + struct mail_index_strmap_header hdr; + const unsigned char *data; +- size_t size; ++ size_t size, hdr_size; + int ret; + + i_assert(strmap->fd == -1); +@@ -263,7 +284,12 @@ static int mail_index_strmap_open(struct + return -1; + } + strmap->input = i_stream_create_fd(strmap->fd, SIZE_MAX); +- ret = i_stream_read_bytes(strmap->input, &data, &size, sizeof(hdr)); ++ ++ /* Read the smaller v1 header first; bytes 0..7 have identical ++ layout in both formats so we can decide based on the version ++ byte before reading the v2 tail. */ ++ ret = i_stream_read_bytes(strmap->input, &data, &size, ++ MAIL_INDEX_STRMAP_HEADER_V1_SIZE); + if (ret <= 0) { + if (ret < 0) { + mail_index_strmap_set_syscall_error(strmap, "read()"); +@@ -274,23 +300,55 @@ static int mail_index_strmap_open(struct + } + return ret; + } +- memcpy(&hdr, data, sizeof(hdr)); ++ ++ i_zero(&hdr); ++ hdr_size = data[0] == MAIL_INDEX_STRMAP_VERSION_V2 ? ++ MAIL_INDEX_STRMAP_HEADER_V2_SIZE : ++ MAIL_INDEX_STRMAP_HEADER_V1_SIZE; ++ if (hdr_size > MAIL_INDEX_STRMAP_HEADER_V1_SIZE) { ++ ret = i_stream_read_bytes(strmap->input, &data, &size, hdr_size); ++ if (ret <= 0) { ++ if (ret < 0) { ++ mail_index_strmap_set_syscall_error(strmap, "read()"); ++ mail_index_strmap_close(strmap); ++ } else { ++ mail_index_strmap_view_set_corrupted(view); ++ } ++ return ret; ++ } ++ } ++ memcpy(&hdr, data, hdr_size); + + idx_hdr = mail_index_get_header(view->view); +- if (hdr.version != MAIL_INDEX_STRMAP_VERSION || +- hdr.uid_validity != idx_hdr->uid_validity) { ++ uint8_t compat_flags = 0; ++#ifndef WORDS_BIGENDIAN ++ if (hdr.version >= MAIL_INDEX_STRMAP_VERSION_V2) ++ compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN; ++#endif ++ if ((hdr.version != MAIL_INDEX_STRMAP_VERSION_V1 && ++ hdr.version != MAIL_INDEX_STRMAP_VERSION_V2) || ++ hdr.compat_flags != compat_flags || ++ hdr.uid_validity != idx_hdr->uid_validity || ++ (strmap->enable_xxh64 && ++ hdr.version < MAIL_INDEX_STRMAP_VERSION_V2)) { + /* need to rebuild. if we already had something in the strmap, +- we can keep it. */ ++ we can keep it. The enable_xxh64 case forces a v1 file to ++ be discarded so the next write creates a v2 file - we never ++ stay on v1 once the dovecot_storage_version threshold has ++ been reached. */ + i_unlink(strmap->path); + mail_index_strmap_close(strmap); + return 0; + } ++ view->format_version = hdr.version; ++ view->hash_iv = hdr.version >= MAIL_INDEX_STRMAP_VERSION_V2 ? ++ hdr.hash_iv : 0; + + /* we'll read the entire file from the beginning */ + view->last_added_uid = 0; + view->last_read_uid = 0; + view->total_ref_count = 0; +- view->last_read_block_offset = sizeof(struct mail_index_strmap_header); ++ view->last_read_block_offset = hdr_size; + view->next_str_idx = 1; + + mail_index_strmap_view_reset(view); +@@ -750,6 +808,17 @@ static inline uint32_t crc32_str_nonzero + return value == 0 ? 1 : value; + } + ++static inline uint32_t ++strmap_hash_str(const struct mail_index_strmap_view *view, const char *str) ++{ ++ if (view->format_version < MAIL_INDEX_STRMAP_VERSION_V2) ++ return crc32_str_nonzero(str); ++ ++ uint32_t value = xxh64_to_32(xxh64_data(str, strlen(str), view->hash_iv)); ++ /* 0 is reserved as a sentinel meaning "unique / no hash stored" */ ++ return value == 0 ? 1 : value; ++} ++ + void mail_index_strmap_view_sync_add(struct mail_index_strmap_view_sync *sync, + uint32_t uid, uint32_t ref_index, + const char *key) +@@ -764,7 +833,7 @@ void mail_index_strmap_view_sync_add(str + ref_index > view->last_ref_index)); + + hash_key.str = key; +- hash_key.hash = crc32_str_nonzero(key); ++ hash_key.hash = strmap_hash_str(view, key); + + old_rec = hash2_lookup(view->hash, &hash_key); + if (old_rec != NULL) { +@@ -971,14 +1040,29 @@ mail_index_strmap_recreate_write(struct + { + const struct mail_index_header *idx_hdr; + struct mail_index_strmap_header hdr; ++ size_t hdr_size; + + idx_hdr = mail_index_get_header(view->view); + ++ /* view->format_version was set either at view_open() time (from the ++ strmap-wide enable_xxh64 preference for fresh files) or in ++ mail_index_strmap_open() from the existing file's header. Reuse ++ it as-is so renumber recreates preserve the file's format. */ ++ + /* write header */ + i_zero(&hdr); +- hdr.version = MAIL_INDEX_STRMAP_VERSION; ++ hdr.version = view->format_version; + hdr.uid_validity = idx_hdr->uid_validity; +- o_stream_nsend(output, &hdr, sizeof(hdr)); ++ if (view->format_version >= MAIL_INDEX_STRMAP_VERSION_V2) { ++#ifndef WORDS_BIGENDIAN ++ hdr.compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN; ++#endif ++ hdr.hash_iv = view->hash_iv; ++ hdr_size = MAIL_INDEX_STRMAP_HEADER_V2_SIZE; ++ } else { ++ hdr_size = MAIL_INDEX_STRMAP_HEADER_V1_SIZE; ++ } ++ o_stream_nsend(output, &hdr, hdr_size); + + view->total_ref_count = 0; + mail_index_strmap_write_block(view, output, 0, 1); +Index: trixie/src/lib-index/mail-index-strmap.h +=================================================================== +--- trixie.orig/src/lib-index/mail-index-strmap.h ++++ trixie/src/lib-index/mail-index-strmap.h +@@ -7,12 +7,22 @@ struct mail_index; + struct mail_index_view; + + struct mail_index_strmap_header { +-#define MAIL_INDEX_STRMAP_VERSION 1 ++/* On-disk format version. The header struct below describes version 2; ++ version 1 files use the same first 8 bytes (version + 3 unused + uid_validity) ++ without the trailing compat_flags / unused / hash_iv fields. */ ++#define MAIL_INDEX_STRMAP_VERSION_V1 1 ++#define MAIL_INDEX_STRMAP_VERSION_V2 2 + uint8_t version; +- uint8_t unused[3]; ++ uint8_t compat_flags; /* enum mail_index_header_compat_flags, v2+ */ ++ uint8_t unused[2]; + + uint32_t uid_validity; ++ ++ /* v2+ fields - not present on disk for v1 files. */ ++ uint64_t hash_iv; /* per-file random IV mixed into the xxh64 hash */ + }; ++#define MAIL_INDEX_STRMAP_HEADER_V1_SIZE 8 ++#define MAIL_INDEX_STRMAP_HEADER_V2_SIZE sizeof(struct mail_index_strmap_header) + + struct mail_index_strmap_rec { + uint32_t uid; +@@ -40,7 +50,8 @@ typedef void mail_index_strmap_remap_t(c + unsigned int new_count, void *context); + + struct mail_index_strmap * +-mail_index_strmap_init(struct mail_index *index, const char *suffix); ++mail_index_strmap_init(struct mail_index *index, const char *suffix, ++ bool enable_xxh64); + void mail_index_strmap_deinit(struct mail_index_strmap **strmap); + + /* Returns strmap records and hash that can be used for read-only access. +Index: trixie/src/lib-master/Makefile.am +=================================================================== +--- trixie.orig/src/lib-master/Makefile.am ++++ trixie/src/lib-master/Makefile.am +@@ -39,6 +39,7 @@ headers = \ + master-service-ssl.h \ + service-settings.h \ + stats-client.h \ ++ storage-version.h \ + syslog-util.h + + pkginc_libdir=$(pkgincludedir) +Index: trixie/src/lib-master/storage-version.h +=================================================================== +--- /dev/null ++++ trixie/src/lib-master/storage-version.h +@@ -0,0 +1,22 @@ ++#ifndef STORAGE_VERSION_H ++#define STORAGE_VERSION_H ++ ++#include "version.h" ++ ++/* Returns TRUE if dovecot_storage_version selects the v2 (keyed xxh64) ++ mail_index_strmap on-disk format. */ ++static inline bool ++storage_version_has_mail_index_strmap_v2(const char *version) ++{ ++ if (version == NULL) { ++ /* unit test */ ++ return TRUE; ++ } ++#ifdef DOVECOT_PRO_EDITION ++ return version_cmp(version, "3.1.6") >= 0; ++#else ++ return version_cmp(version, "2.4.0") >= 0; ++#endif ++} ++ ++#endif +Index: trixie/src/lib-storage/index/index-thread.c +=================================================================== +--- trixie.orig/src/lib-storage/index/index-thread.c ++++ trixie/src/lib-storage/index/index-thread.c +@@ -7,6 +7,9 @@ + #include "bsearch-insert-pos.h" + #include "hash2.h" + #include "message-id.h" ++#include "master-service.h" ++#include "master-service-settings.h" ++#include "storage-version.h" + #include "mail-search.h" + #include "mail-search-build.h" + #include "mailbox-search-result-private.h" +@@ -668,8 +671,15 @@ void index_thread_mailbox_opened(struct + box->v.close = mail_thread_mailbox_close; + box->v.free = mail_thread_mailbox_free; + ++ const struct master_service_settings *master_set = ++ master_service_get_service_settings(master_service); ++ bool use_xxh64 = master_set != NULL && ++ storage_version_has_mail_index_strmap_v2( ++ master_set->dovecot_storage_version); ++ + tbox->strmap = mail_index_strmap_init(box->index, +- MAIL_THREAD_INDEX_SUFFIX); ++ MAIL_THREAD_INDEX_SUFFIX, ++ use_xxh64); + tbox->next_msgid_idx = 1; + + tbox->cache = i_new(struct mail_thread_cache, 1); diff --git a/patches/0007-lib-mail-istream-header-filter-Use-container_of-for-.patch b/patches/0007-lib-mail-istream-header-filter-Use-container_of-for-.patch new file mode 100644 index 0000000..e0752d7 --- /dev/null +++ b/patches/0007-lib-mail-istream-header-filter-Use-container_of-for-.patch @@ -0,0 +1,65 @@ +From c5996d464e31ee9950fd40654e9fedcf9f99719e Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 20 Apr 2026 01:14:02 +0300 +Subject: [PATCH 07/14] lib-mail: istream-header-filter - Use container_of() + for struct casts + +Replace the C-style downcasts from struct istream_private/iostream_private +to struct header_filter_istream with container_of(), matching the +convention used by other istream implementations in tree. +--- + src/lib-mail/istream-header-filter.c | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/src/lib-mail/istream-header-filter.c b/src/lib-mail/istream-header-filter.c +index ee94e4f82b..5fee9cf17b 100644 +--- a/src/lib-mail/istream-header-filter.c ++++ b/src/lib-mail/istream-header-filter.c +@@ -60,7 +60,7 @@ static ssize_t i_stream_header_filter_read(struct istream_private *stream); + static void i_stream_header_filter_destroy(struct iostream_private *stream) + { + struct header_filter_istream *mstream = +- (struct header_filter_istream *)stream; ++ container_of(stream, struct header_filter_istream, istream.iostream); + + if (mstream->hdr_ctx != NULL) + message_parse_header_deinit(&mstream->hdr_ctx); +@@ -470,7 +470,7 @@ handle_end_body_with_lf(struct header_filter_istream *mstream, ssize_t ret) + static ssize_t i_stream_header_filter_read(struct istream_private *stream) + { + struct header_filter_istream *mstream = +- (struct header_filter_istream *)stream; ++ container_of(stream, struct header_filter_istream, istream); + uoff_t v_offset; + ssize_t ret; + +@@ -554,7 +554,7 @@ static void i_stream_header_filter_seek(struct istream_private *stream, + uoff_t v_offset, bool mark ATTR_UNUSED) + { + struct header_filter_istream *mstream = +- (struct header_filter_istream *)stream; ++ container_of(stream, struct header_filter_istream, istream); + + if (stream->istream.v_offset == v_offset) { + /* just reset the input buffer */ +@@ -604,7 +604,7 @@ static int + i_stream_header_filter_stat(struct istream_private *stream, bool exact) + { + struct header_filter_istream *mstream = +- (struct header_filter_istream *)stream; ++ container_of(stream, struct header_filter_istream, istream); + const struct stat *st; + uoff_t old_offset; + +@@ -673,7 +673,7 @@ i_stream_header_filter_snapshot(struct istream_private *stream, + struct istream_snapshot *prev_snapshot) + { + struct header_filter_istream *mstream = +- (struct header_filter_istream *)stream; ++ container_of(stream, struct header_filter_istream, istream); + struct header_filter_istream_snapshot *snapshot; + + if (stream->buffer != mstream->hdr_buf->data) { +-- +2.47.3 + diff --git a/patches/0007-lib-sieve-Log-an-info-line-when-opening-a-CPU-disabl.patch b/patches/0007-lib-sieve-Log-an-info-line-when-opening-a-CPU-disabl.patch new file mode 100644 index 0000000..ee6fbe1 --- /dev/null +++ b/patches/0007-lib-sieve-Log-an-info-line-when-opening-a-CPU-disabl.patch @@ -0,0 +1,52 @@ +From 800697eaa3b753d7a449fb7da62f41a21b02925e Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 3 Jun 2026 12:25:49 +0000 +Subject: [PATCH 7/7] lib-sieve: Log an info line when opening a CPU-disabled + script + +sieve_binary_check_executable() blocks execution when a script's +persisted cumulative CPU usage already exceeds sieve_max_cpu_time, but +only logged the block at debug level. With cumulative tracking now +sticky across redeliveries via the per-user sieve-rusage file, the +script can stay blocked indefinitely until the resource_usage_timeout +decay or admin intervention; operators benefit from a per-attempt +record of that block at info level. + +Promote the existing debug log to e_info and include the cumulative +CPU usage. The caller continues to emit its own error message on the +failed open (lda-sieve, imap-sieve, imap-filter-sieve plugin paths). +--- + src/lib-sieve/sieve-binary-file.c | 14 +++++++++++--- + 1 file changed, 11 insertions(+), 3 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-file.c ++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c +@@ -935,15 +935,23 @@ int sieve_binary_check_executable(struct + const char **client_error_r) + { + struct sieve_resource_usage rusage; ++ struct sieve_script *script = sbin->script; + + *client_error_r = NULL; + sieve_error_args_init(&error_code_r, NULL); + + sieve_binary_get_resource_usage(sbin, &rusage); + if (sieve_resource_usage_is_excessive(sbin->svinst, &rusage)) { +- e_debug(sbin->event, +- "Binary execution is blocked: " +- "Cumulative resource usage limit exceeded"); ++ /* Cumulative resource usage is only tracked for personal ++ scripts. */ ++ i_assert(script != NULL); ++ i_assert(sieve_storage_is_personal(script->storage)); ++ ++ e_info(sbin->event, ++ "Personal sieve processing disabled for script '%s': " ++ "cumulative resource usage limit exceeded (%s)", ++ sieve_script_label(script), ++ sieve_resource_usage_get_summary(&rusage)); + *error_code_r = SIEVE_ERROR_RESOURCE_LIMIT; + *client_error_r = "cumulative resource usage limit exceeded"; + return 0; diff --git a/patches/0007-lib-sql-Run-sql-statement-queries-in-their-own-data-.patch b/patches/0007-lib-sql-Run-sql-statement-queries-in-their-own-data-.patch new file mode 100644 index 0000000..a1175af --- /dev/null +++ b/patches/0007-lib-sql-Run-sql-statement-queries-in-their-own-data-.patch @@ -0,0 +1,104 @@ +From 30ec9bdcb445011801229a825c1d5a443397463f Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 11 Jun 2026 20:02:15 +0000 +Subject: [PATCH] lib-sql: Run sql statement queries in their own data stack + frame + +--- + src/lib-sql/sql-api.c | 62 +++++++++++++++++++++++++------------------ + 1 file changed, 36 insertions(+), 26 deletions(-) + +Index: trixie/src/lib-sql/sql-api.c +=================================================================== +--- trixie.orig/src/lib-sql/sql-api.c ++++ trixie/src/lib-sql/sql-api.c +@@ -657,31 +657,37 @@ void sql_statement_query(struct sql_stat + struct sql_statement *stmt = *_stmt; + *_stmt = NULL; + +- if (stmt->db->v.statement_query != NULL) +- stmt->db->v.statement_query(stmt, callback, context); +- else if (stmt->db->v.statement_query_s != NULL) { +- struct sql_db *db = stmt->db; +- struct sql_query_result_delayed *cb = +- i_new(struct sql_query_result_delayed, 1); +- cb->db = db; +- cb->callback = callback; +- cb->context = context; +- cb->result = sql_statement_query_s(&stmt); +- cb->to = timeout_add_short(0, sql_query_delayed_callback, cb); +- DLLIST_PREPEND(&db->query_delayed_list, cb); +- } else +- default_sql_statement_query(stmt, callback, context); ++ T_BEGIN { ++ if (stmt->db->v.statement_query != NULL) ++ stmt->db->v.statement_query(stmt, callback, context); ++ else if (stmt->db->v.statement_query_s != NULL) { ++ struct sql_db *db = stmt->db; ++ struct sql_query_result_delayed *cb = ++ i_new(struct sql_query_result_delayed, 1); ++ cb->db = db; ++ cb->callback = callback; ++ cb->context = context; ++ cb->result = sql_statement_query_s(&stmt); ++ cb->to = timeout_add_short(0, sql_query_delayed_callback, cb); ++ DLLIST_PREPEND(&db->query_delayed_list, cb); ++ } else ++ default_sql_statement_query(stmt, callback, context); ++ } T_END; + } + + struct sql_result *sql_statement_query_s(struct sql_statement **_stmt) + { + struct sql_statement *stmt = *_stmt; ++ struct sql_result *result; + + *_stmt = NULL; +- if (stmt->db->v.statement_query_s != NULL) +- return stmt->db->v.statement_query_s(stmt); +- else +- return default_sql_statement_query_s(stmt); ++ T_BEGIN { ++ if (stmt->db->v.statement_query_s != NULL) ++ result = stmt->db->v.statement_query_s(stmt); ++ else ++ result = default_sql_statement_query_s(stmt); ++ } T_END; ++ return result; + } + + void sql_result_ref(struct sql_result *result) +@@ -1011,10 +1017,12 @@ void sql_update_stmt(struct sql_transact + struct sql_statement *stmt = *_stmt; + + *_stmt = NULL; +- if (ctx->db->v.update_stmt != NULL) +- ctx->db->v.update_stmt(ctx, stmt, NULL); +- else +- default_sql_update_stmt(ctx, stmt, NULL); ++ T_BEGIN { ++ if (ctx->db->v.update_stmt != NULL) ++ ctx->db->v.update_stmt(ctx, stmt, NULL); ++ else ++ default_sql_update_stmt(ctx, stmt, NULL); ++ } T_END; + } + + void sql_update_get_rows(struct sql_transaction_context *ctx, const char *query, +@@ -1030,10 +1038,12 @@ void sql_update_stmt_get_rows(struct sql + struct sql_statement *stmt = *_stmt; + + *_stmt = NULL; +- if (ctx->db->v.update_stmt != NULL) +- ctx->db->v.update_stmt(ctx, stmt, affected_rows); +- else +- default_sql_update_stmt(ctx, stmt, affected_rows); ++ T_BEGIN { ++ if (ctx->db->v.update_stmt != NULL) ++ ctx->db->v.update_stmt(ctx, stmt, affected_rows); ++ else ++ default_sql_update_stmt(ctx, stmt, affected_rows); ++ } T_END; + } + + void sql_db_set_state(struct sql_db *db, enum sql_db_state state) diff --git a/patches/0008-doveadm-Fix-ubsan-unsigned-integer-overflow-error.patch b/patches/0008-doveadm-Fix-ubsan-unsigned-integer-overflow-error.patch new file mode 100644 index 0000000..5c1c4d7 --- /dev/null +++ b/patches/0008-doveadm-Fix-ubsan-unsigned-integer-overflow-error.patch @@ -0,0 +1,29 @@ +From aba5b8556952dbb47b9ed8b338d170cb73c80ce6 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 16 Apr 2026 13:27:42 +0200 +Subject: [PATCH 08/12] doveadm: Fix ubsan unsigned integer overflow error + +--- + src/doveadm/doveadm-who.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/doveadm/doveadm-who.c b/src/doveadm/doveadm-who.c +index 2fbef7bb94..ecdbb88bc7 100644 +--- a/src/doveadm/doveadm-who.c ++++ b/src/doveadm/doveadm-who.c +@@ -49,10 +49,10 @@ static unsigned int who_user_hash(const struct who_user *user) + unsigned int hash = str_hash(user->service); + + if (user->username[0] != '\0') +- hash += str_hash(user->username); ++ hash ^= str_hash(user->username); + else { + who_user_ip(user, &ip); +- hash += net_ip_hash(&ip); ++ hash ^= net_ip_hash(&ip); + } + return hash; + } +-- +2.47.3 + diff --git a/patches/0008-lib-mail-Make-istream-header-filter-s-per-header-siz.patch b/patches/0008-lib-mail-Make-istream-header-filter-s-per-header-siz.patch new file mode 100644 index 0000000..241eb02 --- /dev/null +++ b/patches/0008-lib-mail-Make-istream-header-filter-s-per-header-siz.patch @@ -0,0 +1,91 @@ +From a444863fef605978c6f18e85db07f382d6f21d23 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sun, 19 Apr 2026 18:15:49 +0300 +Subject: [PATCH 08/14] lib-mail: Make istream-header-filter's per-header size + limit configurable + +Until now the internal message_header_parser_ctx used the default +MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE (10 MB) cap. In practice that cap +only bounded hdr->full_value; per-chunk hdr->value was delivered in full +regardless. A subsequent change to message_parse_header_next() will +clamp hdr->value cumulatively as well, and at that point the 10 MB +default would silently truncate data for callers like mbox-save that +stream raw header bytes to storage. + +Default the filter to SIZE_MAX (unlimited) to preserve the effective +behavior and add i_stream_header_filter_set_max_header_block_size() so +callers that genuinely want a cap (index_mail_get_header_stream()) can +opt in. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + src/lib-mail/istream-header-filter.c | 20 ++++++++++++++++++++ + src/lib-mail/istream-header-filter.h | 8 ++++++++ + 2 files changed, 28 insertions(+) + +Index: trixie/src/lib-mail/istream-header-filter.c +=================================================================== +--- trixie.orig/src/lib-mail/istream-header-filter.c ++++ trixie/src/lib-mail/istream-header-filter.c +@@ -29,6 +29,7 @@ struct header_filter_istream { + buffer_t *hdr_buf; + int snapshot_pending_refcount; + struct message_size header_size; ++ size_t max_header_block_size; + uoff_t skip_count; + uoff_t last_lf_offset; + +@@ -194,6 +195,8 @@ static ssize_t read_header(struct header + mstream->hdr_ctx = + message_parse_header_init(mstream->istream.parent, + NULL, 0); ++ message_parse_header_set_limit(mstream->hdr_ctx, ++ mstream->max_header_block_size); + } + + /* remove skipped data from hdr_buf */ +@@ -709,6 +712,9 @@ i_stream_create_header_filter(struct ist + mstream->pool = pool_alloconly_create(MEMPOOL_GROWING + "header filter stream", 256); + mstream->istream.max_buffer_size = input->real_stream->max_buffer_size; ++ /* Default to no per-header block size limit. Callers that want one ++ call i_stream_header_filter_set_max_header_block_size(). */ ++ mstream->max_header_block_size = SIZE_MAX; + + mstream->headers = headers_count == 0 ? NULL : + p_new(mstream->pool, const char *, headers_count); +@@ -762,3 +768,17 @@ void i_stream_header_filter_add(struct h + buffer_append(input->hdr_buf, data, size); + input->headers_edited = TRUE; + } ++ ++void i_stream_header_filter_set_max_header_block_size( ++ struct istream *input, size_t max_header_block_size) ++{ ++ struct header_filter_istream *mstream = ++ container_of(input->real_stream, struct header_filter_istream, ++ istream); ++ ++ mstream->max_header_block_size = max_header_block_size; ++ if (mstream->hdr_ctx != NULL) { ++ message_parse_header_set_limit(mstream->hdr_ctx, ++ max_header_block_size); ++ } ++} +Index: trixie/src/lib-mail/istream-header-filter.h +=================================================================== +--- trixie.orig/src/lib-mail/istream-header-filter.h ++++ trixie/src/lib-mail/istream-header-filter.h +@@ -49,4 +49,12 @@ i_stream_create_header_filter(struct ist + void i_stream_header_filter_add(struct header_filter_istream *input, + const void *data, size_t size); + ++/* Set a per-header-block maximum size for this filter stream. Data beyond ++ this limit in any single header is dropped from the filter's output and ++ from values delivered to the filter callback. The default is SIZE_MAX ++ (unlimited). Use MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE from ++ message-header-parser.h for the standard cap. */ ++void i_stream_header_filter_set_max_header_block_size( ++ struct istream *input, size_t max_header_block_size); ++ + #endif diff --git a/patches/0008-lib-sieve-Reduce-CPU-limit-check-interval-in-contain.patch b/patches/0008-lib-sieve-Reduce-CPU-limit-check-interval-in-contain.patch new file mode 100644 index 0000000..e3cb1c5 --- /dev/null +++ b/patches/0008-lib-sieve-Reduce-CPU-limit-check-interval-in-contain.patch @@ -0,0 +1,40 @@ +From 01c95a5503ec53c40f12d40de0b508838f6b041d Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 25 May 2026 14:54:03 +0000 +Subject: [PATCH] lib-sieve: Reduce CPU limit check interval in :contains and + :matches matchers + +Reduce the interval to 64 so worst-case work between checks is ~64 MiB. +cpu_limit_exceeded() is a signal-counter comparison with no syscall, so +the higher call frequency has no measurable cost. +--- + src/lib-sieve/mcht-contains.c | 2 +- + src/lib-sieve/mcht-matches.c | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +Index: trixie/pigeonhole/src/lib-sieve/mcht-contains.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/mcht-contains.c ++++ trixie/pigeonhole/src/lib-sieve/mcht-contains.c +@@ -45,7 +45,7 @@ const struct sieve_match_type_def contai + * configured sieve_max_cpu_time (which is otherwise only checked between + * bytecode operations). + */ +-#define SIEVE_CONTAINS_CPU_CHECK_INTERVAL 4096 ++#define SIEVE_CONTAINS_CPU_CHECK_INTERVAL 64 + + static int mcht_contains_match_key + (struct sieve_match_context *mctx, const char *val, size_t val_size, +Index: trixie/pigeonhole/src/lib-sieve/mcht-matches.c +=================================================================== +--- trixie.orig/pigeonhole/src/lib-sieve/mcht-matches.c ++++ trixie/pigeonhole/src/lib-sieve/mcht-matches.c +@@ -53,7 +53,7 @@ const struct sieve_match_type_def matche + * configured sieve_max_cpu_time. Returns 1 on match, 0 on exhaustion, or -1 + * when the CPU time limit was exceeded (mctx->exec_status is set). + */ +-#define SIEVE_MATCHES_CPU_CHECK_INTERVAL 4096 ++#define SIEVE_MATCHES_CPU_CHECK_INTERVAL 64 + + static int + _string_find(struct sieve_match_context *mctx, diff --git a/patches/0009-config-Assume-dovecot_config_version-0.0.0-is-the-sa.patch b/patches/0009-config-Assume-dovecot_config_version-0.0.0-is-the-sa.patch new file mode 100644 index 0000000..9a07818 --- /dev/null +++ b/patches/0009-config-Assume-dovecot_config_version-0.0.0-is-the-sa.patch @@ -0,0 +1,92 @@ +From f5b9e0c2310b6fa744cf098dea9c03f88a985898 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 29 Sep 2025 14:09:42 +0300 +Subject: [PATCH 09/12] config: Assume dovecot_config_version=0.0.0 is the same + as the latest version + +It's used for git builds. +--- + src/config/config-parser.c | 16 +++++++++++++++- + src/config/config-parser.h | 3 +++ + src/config/doveconf.c | 10 +++++++++- + 3 files changed, 27 insertions(+), 2 deletions(-) + +diff --git a/src/config/config-parser.c b/src/config/config-parser.c +index b2883538ae..7e9db599f4 100644 +--- a/src/config/config-parser.c ++++ b/src/config/config-parser.c +@@ -2604,6 +2604,17 @@ static bool config_version_find(const char *version, const char **error_r) + return TRUE; + } + ++static bool ++dovecot_config_version_equals(struct config_parser_context *ctx, const char *value) ++{ ++ if (strcmp(ctx->dovecot_config_version, value) == 0) ++ return TRUE; ++ if (strcmp(ctx->dovecot_config_version, CONFIG_VERSION_MAX) == 0 && ++ strcmp(value, CONFIG_VERSION_GIT) == 0) ++ return TRUE; ++ return FALSE; ++} ++ + static bool config_parser_get_version(struct config_parser_context *ctx, + const struct config_line *line) + { +@@ -2616,7 +2627,7 @@ static bool config_parser_get_version(struct config_parser_context *ctx, + if (strcmp(line->key, "dovecot_config_version") == 0) { + if (ctx->dovecot_config_version == NULL) + ; +- else if (strcmp(ctx->dovecot_config_version, line->value) != 0) { ++ else if (!dovecot_config_version_equals(ctx, line->value)) { + ctx->error = "dovecot_config_version value can't be changed once set"; + return TRUE; + } else { +@@ -2636,6 +2647,9 @@ static bool config_parser_get_version(struct config_parser_context *ctx, + else if (!config_version_find(line->value, &error)) { + ctx->error = p_strdup_printf(ctx->pool, + "Invalid dovecot_config_version: %s", error); ++ } else if (strcmp(line->value, CONFIG_VERSION_GIT) == 0) { ++ /* git build - this is the same as the latest version. */ ++ ctx->dovecot_config_version = CONFIG_VERSION_MAX; + } else { + ctx->dovecot_config_version = p_strdup(ctx->pool, line->value); + } +diff --git a/src/config/config-parser.h b/src/config/config-parser.h +index 949aac100b..f9d70f1be8 100644 +--- a/src/config/config-parser.h ++++ b/src/config/config-parser.h +@@ -5,6 +5,9 @@ + + #define CONFIG_MODULE_DIR MODULEDIR"/settings" + ++#define CONFIG_VERSION_GIT "0.0.0" ++#define CONFIG_VERSION_MAX "9999.9999.9999" ++ + /* change_counter used for default settings created internally */ + #define CONFIG_PARSER_CHANGE_DEFAULTS 1 + /* change_counter used for settings changed by configuration file */ +diff --git a/src/config/doveconf.c b/src/config/doveconf.c +index 17dafbf006..825d84545c 100644 +--- a/src/config/doveconf.c ++++ b/src/config/doveconf.c +@@ -1297,7 +1297,15 @@ int main(int argc, char *argv[]) + printf("# %u default setting changes since version %s\n", + count, version); + } +- printf("dovecot_config_version = %s\n", version); ++ if (strcmp(version, CONFIG_VERSION_MAX) != 0) ++ printf("dovecot_config_version = %s\n", version); ++ else { ++ /* GIT version was changed to MAX for easier ++ comparisons internally. However, output it ++ back as the original GIT version. */ ++ printf("dovecot_config_version = %s\n", ++ CONFIG_VERSION_GIT); ++ } + } + if (!config_path_specified) + check_wrong_config(config_path); +-- +2.47.3 + diff --git a/patches/0009-lib-storage-Cap-per-header-size-in-index_mail_get_he.patch b/patches/0009-lib-storage-Cap-per-header-size-in-index_mail_get_he.patch new file mode 100644 index 0000000..4ae0dca --- /dev/null +++ b/patches/0009-lib-storage-Cap-per-header-size-in-index_mail_get_he.patch @@ -0,0 +1,42 @@ +From 6802995e1302718a81bc1e39bdd43d8296421d11 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sun, 19 Apr 2026 18:28:16 +0300 +Subject: [PATCH 09/14] lib-storage: Cap per-header size in + index_mail_get_header_stream() + +Apply MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE (10 MB) to the header-filter +istream used for populating the header cache. A pathological single +header (for example a To: with millions of addresses) otherwise grows +mail->header_data and the cache write buffer in lockstep with the raw +header size, which can push the imap process over vsz_limit on FETCH +ENVELOPE / BODYSTRUCTURE. + +On its own this change does not yet bound hdr->value delivery; that +requires the upcoming change to message_parse_header_next() to clamp +per-chunk value_len cumulatively. Setting the limit here now lets that +follow-up take effect without further touching this file. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + src/lib-storage/index/index-mail-headers.c | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/src/lib-storage/index/index-mail-headers.c b/src/lib-storage/index/index-mail-headers.c +index 18c922239d..a9f4d55c0d 100644 +--- a/src/lib-storage/index/index-mail-headers.c ++++ b/src/lib-storage/index/index-mail-headers.c +@@ -992,6 +992,11 @@ int index_mail_get_header_stream(struct mail *_mail, + HEADER_FILTER_HIDE_BODY, + headers->name, headers->count, + header_cache_callback, mail); ++ /* Cap per-header data so a single pathological header cannot exhaust ++ memory in mail->header_data / the filter's buffer. */ ++ i_stream_header_filter_set_max_header_block_size( ++ mail->data.filter_stream, ++ MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE); + *stream_r = mail->data.filter_stream; + return 0; + } +-- +2.47.3 + diff --git a/patches/0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch b/patches/0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch new file mode 100644 index 0000000..18a4332 --- /dev/null +++ b/patches/0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch @@ -0,0 +1,123 @@ +From 64ab59e17533a7eb63cbd4cd8cce81a39b32b2b1 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 20 Apr 2026 00:16:14 +0300 +Subject: [PATCH 10/14] lib-mail: Clamp hdr->value cumulatively against + header_block_max_size + +Until now header_block_max_size only bounded hdr->full_value via +value_buf. Continued chunks returned to the caller via hdr->value were +left at the raw chunk size, so a caller that consumed hdr->value per +chunk without ever requesting use_full_value (e.g. the header-cache +path in index_mail_parse_header()) could accumulate the full raw header +size. A pathological To: with millions of addresses could grow +mail->header_data and the cache write buffer to tens of megabytes each, +driving the imap process over vsz_limit on FETCH ENVELOPE. + +Reinterpret header_block_total_size as the running sum of line_value_size +across all chunks of all headers, and clamp each new chunk against the +remaining header_block_max_size budget. Propagate the clamped size to +line->value_len in the two continued-line branches that previously left +it untouched. value_buf is bounded implicitly since every append uses +line_value_size. The up-front per-chunk clamp +(line->value_len = MIN(value_len, max_size)) is now subsumed by the +cumulative clamp and has been removed. + +Update the truncation tests that were documenting the old +"value_len stays at raw chunk size" behavior. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + src/lib-mail/message-header-parser.c | 16 +++++++++++----- + src/lib-mail/test-message-header-parser.c | 16 ++++++++++++---- + 2 files changed, 23 insertions(+), 9 deletions(-) + +diff --git a/src/lib-mail/message-header-parser.c b/src/lib-mail/message-header-parser.c +index b240a47884..3026a35f29 100644 +--- a/src/lib-mail/message-header-parser.c ++++ b/src/lib-mail/message-header-parser.c +@@ -96,7 +96,6 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx, + /* new header line */ + line->name_offset = ctx->input->v_offset; + colon_pos = UINT_MAX; +- ctx->header_block_total_size += ctx->value_buf->used; + buffer_set_used_size(ctx->value_buf, 0); + } + +@@ -362,12 +361,17 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx, + } + } + +- line->value_len = I_MIN(line->value_len, ctx->header_block_max_size); + size_t line_value_size = line->value_len; +- size_t header_total_used = ctx->header_block_total_size + ctx->value_buf->used; +- size_t line_available = ctx->header_block_max_size <= header_total_used ? 0 : +- ctx->header_block_max_size - header_total_used; ++ /* Clamp line_value_size against the remaining header_block_max_size ++ budget. header_block_total_size is the running sum of ++ line_value_size across all chunks of all headers; value_buf growth ++ is bounded implicitly because every append to value_buf uses ++ line_value_size. */ ++ size_t line_available = ++ ctx->header_block_max_size <= ctx->header_block_total_size ? 0 : ++ ctx->header_block_max_size - ctx->header_block_total_size; + line_value_size = I_MIN(line_value_size, line_available); ++ ctx->header_block_total_size += line_value_size; + + if (!line->continued) { + /* first header line. make a copy of the line since we can't +@@ -397,10 +401,12 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx, + + line->full_value = ctx->value_buf->data; + line->full_value_len = ctx->value_buf->used; ++ line->value_len = line_value_size; + } else { + /* we didn't want full_value, and this is a continued line. */ + line->full_value = NULL; + line->full_value_len = 0; ++ line->value_len = line_value_size; + } + + /* always reset it */ +diff --git a/src/lib-mail/test-message-header-parser.c b/src/lib-mail/test-message-header-parser.c +index 5395c54ed6..f2c9184e66 100644 +--- a/src/lib-mail/test-message-header-parser.c ++++ b/src/lib-mail/test-message-header-parser.c +@@ -494,11 +494,14 @@ static void test_message_header_truncation_clean_oneline(void) + struct message_header_parser_ctx *parser = message_parse_header_init(input, NULL, MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE); + message_parse_header_set_limit(parser, 96); + ++ /* hdr->value is now also clamped cumulatively against the same ++ header_block_max_size budget as full_value, so later chunks of a ++ multiline header shrink or disappear once the budget is used up. */ + assert_parse_line( 1, "header1", "this is short", "this is short"); + assert_parse_line( 2, "header2", "this is multiline", "this is multiline"); + assert_parse_line( 3, "header2", " and long 343638404244464850525456586062", "this is multiline and long 343638404244464850525456586062"); +- assert_parse_line( 4, "header2", " 64666870727476788082848688909294969800", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); +- assert_parse_line( 5, "header2", " 02040608101214161820222426283032343638", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); ++ assert_parse_line( 4, "header2", " 6466687072747678808284868", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); ++ assert_parse_line( 5, "header2", "", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); + assert_parse_line( 6, "header3", "", ""); + test_assert(message_parse_header_next(parser, &hdr) > 0 && hdr->eoh); + +@@ -515,11 +518,16 @@ static void test_message_header_truncation_flag0(void) + struct message_header_parser_ctx *parser = message_parse_header_init(input, NULL, 0); + message_parse_header_set_limit(parser, 96); + ++ /* hdr->value is clamped cumulatively against header_block_max_size. ++ The \n that NO flags inserts between continuations goes into ++ value_buf but is not added to header_block_total_size, so line 4's ++ value hits the same 26-byte cap as in CLEAN_ONELINE and ++ full_value_len is one byte larger than the nominal limit. */ + assert_parse_line( 1, "header1", "this is short", "this is short"); + assert_parse_line( 2, "header2", "this is multiline", "this is multiline"); + assert_parse_line( 3, "header2", " and long 343638404244464850525456586062", "this is multiline\n and long 343638404244464850525456586062"); +- assert_parse_line( 4, "header2", " 64666870727476788082848688909294969800", "this is multiline\n and long 343638404244464850525456586062\n 646668707274767880828486"); +- assert_parse_line( 5, "header2", " 02040608101214161820222426283032343638", "this is multiline\n and long 343638404244464850525456586062\n 646668707274767880828486"); ++ assert_parse_line( 4, "header2", " 6466687072747678808284868", "this is multiline\n and long 343638404244464850525456586062\n 6466687072747678808284868"); ++ assert_parse_line( 5, "header2", "", "this is multiline\n and long 343638404244464850525456586062\n 6466687072747678808284868"); + assert_parse_line( 6, "header3", "", ""); + test_assert(message_parse_header_next(parser, &hdr) > 0 && hdr->eoh); + +-- +2.47.3 + diff --git a/patches/0010-lib-master-Move-config_version_find-from-src-config-.patch b/patches/0010-lib-master-Move-config_version_find-from-src-config-.patch new file mode 100644 index 0000000..b11074d --- /dev/null +++ b/patches/0010-lib-master-Move-config_version_find-from-src-config-.patch @@ -0,0 +1,123 @@ +From 9d9efd5ea41e0e3183e1e53d4326f40b65dd9ef8 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 1 May 2026 16:56:21 +0000 +Subject: [PATCH 10/12] lib-master: Move config_version_find() from + src/config/config-parser.c + +Rename to dovecot_config_version_find() and expose it via +master-service-settings.h so non-config callers (e.g. the upcoming +dovecot_storage_version validation in master_service_settings_check()) +can share the same supported-versions list. No behaviour change. +--- + src/config/config-parser.c | 31 ++++++------------- + src/lib-master/master-service-settings.c | 38 ++++++++++++++++++++++++ + src/lib-master/master-service-settings.h | 5 ++++ + 3 files changed, 52 insertions(+), 22 deletions(-) + +Index: trixie/src/config/config-parser.c +=================================================================== +--- trixie.orig/src/config/config-parser.c ++++ trixie/src/config/config-parser.c +@@ -2580,30 +2580,6 @@ config_parser_check_warnings(struct conf + hash_table_insert(ctx->seen_settings, path, first_pos); + } + +-static bool config_version_find(const char *version, const char **error_r) +-{ +- const char *const supported_versions[] = { +-#ifdef DOVECOT_PRO_EDITION +- "3.1.0", +-#else +- "2.4.0", +-#endif +- NULL +- }; +- /* FIXME: implement full version checking later */ +- if (!str_array_find(supported_versions, version) && +- strcmp(DOVECOT_CONFIG_VERSION, version) != 0) { +- *error_r = t_strdup_printf( +- "Currently supported versions are: %s%s", +- t_strarray_join(supported_versions, " "), +- str_array_find(supported_versions, +- DOVECOT_CONFIG_VERSION) ? "" : +- t_strdup_printf(" %s", DOVECOT_CONFIG_VERSION)); +- return FALSE; +- } +- return TRUE; +-} +- + static bool + dovecot_config_version_equals(struct config_parser_context *ctx, const char *value) + { +@@ -2644,7 +2620,7 @@ static bool config_parser_get_version(st + + if (line->type != CONFIG_LINE_TYPE_KEYVALUE) + ctx->error = "Invalid dovecot_config_version: value is not a string"; +- else if (!config_version_find(line->value, &error)) { ++ else if (!dovecot_config_version_find(line->value, &error)) { + ctx->error = p_strdup_printf(ctx->pool, + "Invalid dovecot_config_version: %s", error); + } else if (strcmp(line->value, CONFIG_VERSION_GIT) == 0) { +Index: trixie/src/lib-master/master-service-settings.c +=================================================================== +--- trixie.orig/src/lib-master/master-service-settings.c ++++ trixie/src/lib-master/master-service-settings.c +@@ -146,6 +146,44 @@ master_service_set_process_shutdown_filt + master_service_set_process_shutdown_filter(master_service, filter); + } + ++/* List of dovecot_config_versions accepted at parse / startup time. ++ dovecot_storage_version accepts these as well, plus an extra legacy set ++ defined in storage_version_check(). */ ++static const char *const dovecot_config_supported_versions[] = { ++#ifdef DOVECOT_PRO_EDITION ++ "3.1.0", ++ "3.1.1", ++ "3.1.2", ++ "3.1.3", ++ "3.1.4", ++ "3.1.5", ++ "3.2.0", ++#else ++ "2.4.0", ++ "2.4.1", ++ "2.4.2", ++ "2.4.3", ++ "2.4.4", ++#endif ++ NULL ++}; ++ ++bool dovecot_config_version_find(const char *version, const char **error_r) ++{ ++ /* FIXME: implement full version checking later */ ++ if (!str_array_find(dovecot_config_supported_versions, version) && ++ strcmp(DOVECOT_CONFIG_VERSION, version) != 0) { ++ *error_r = t_strdup_printf( ++ "Currently supported versions are: %s%s", ++ t_strarray_join(dovecot_config_supported_versions, " "), ++ str_array_find(dovecot_config_supported_versions, ++ DOVECOT_CONFIG_VERSION) ? "" : ++ t_strdup_printf(" %s", DOVECOT_CONFIG_VERSION)); ++ return FALSE; ++ } ++ return TRUE; ++} ++ + static bool storage_version_check(const char *version, const char **error_r) + { + #define STORAGE_MIN_VERSION "2.3.0" +Index: trixie/src/lib-master/master-service-settings.h +=================================================================== +--- trixie.orig/src/lib-master/master-service-settings.h ++++ trixie/src/lib-master/master-service-settings.h +@@ -95,4 +95,9 @@ master_service_get_service_settings(stru + const char * + master_service_get_import_environment_keyvals(struct master_service *service); + ++/* Returns TRUE if version is in the list of dovecot_config_version values ++ accepted by this build. Sets *error_r to a descriptive list of supported ++ versions on FALSE. */ ++bool dovecot_config_version_find(const char *version, const char **error_r); ++ + #endif diff --git a/patches/0011-lib-lib-master-Move-version_-to-lib-version.-ch.patch b/patches/0011-lib-lib-master-Move-version_-to-lib-version.-ch.patch new file mode 100644 index 0000000..8422135 --- /dev/null +++ b/patches/0011-lib-lib-master-Move-version_-to-lib-version.-ch.patch @@ -0,0 +1,591 @@ +From 0e7ba48c290b251b2f264192dd559b72d48a8005 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sun, 5 Oct 2025 18:49:13 +0300 +Subject: [PATCH 11/12] lib, lib-master: Move version_*() to lib/version.[ch] + +--- + src/anvil/anvil-connection.c | 1 + + src/config/config-connection.c | 1 + + src/config/config-parser.c | 3 +- + src/config/old-set-parser.c | 2 +- + src/doveadm/client-connection-tcp.c | 2 +- + src/doveadm/doveadm-penalty.c | 2 +- + src/doveadm/doveadm-stats.c | 2 +- + src/doveadm/doveadm-util.c | 1 + + src/doveadm/doveadm-who.c | 2 +- + src/doveadm/dsync/dsync-ibc-stream.c | 2 +- + src/imap-urlauth-login/imap-urlauth-login.c | 2 +- + src/imap/imap-client-hibernate.c | 2 +- + src/lib-doveadm/doveadm-client.c | 2 +- + src/lib-master/Makefile.am | 5 -- + src/lib-master/master-service-settings.c | 1 + + src/lib-master/master-service.c | 85 ------------------ + src/lib-master/master-service.h | 15 ---- + src/lib/Makefile.am | 3 + + src/lib/test-lib.inc | 1 + + .../test-version.c} | 16 ++-- + src/lib/version.c | 90 +++++++++++++++++++ + src/lib/version.h | 19 ++++ + src/util/script-login.c | 1 + + src/util/script.c | 1 + + 24 files changed, 136 insertions(+), 125 deletions(-) + rename src/{lib-master/test-master-service.c => lib/test-version.c} (83%) + create mode 100644 src/lib/version.c + create mode 100644 src/lib/version.h + +diff --git a/src/anvil/anvil-connection.c b/src/anvil/anvil-connection.c +index 13c57bf3e7..9cd44563ed 100644 +--- a/src/anvil/anvil-connection.c ++++ b/src/anvil/anvil-connection.c +@@ -11,6 +11,7 @@ + #include "connection.h" + #include "str.h" + #include "strescape.h" ++#include "version.h" + #include "master-service.h" + #include "master-interface.h" + #include "connect-limit.h" +diff --git a/src/config/config-connection.c b/src/config/config-connection.c +index d9e0a13207..265c702601 100644 +--- a/src/config/config-connection.c ++++ b/src/config/config-connection.c +@@ -7,6 +7,7 @@ + #include "ostream.h" + #include "ostream-unix.h" + #include "strescape.h" ++#include "version.h" + #include "settings-parser.h" + #include "master-service.h" + #include "master-service-settings.h" +diff --git a/src/config/config-parser.c b/src/config/config-parser.c +index 55e124e88b..07dca715f9 100644 +--- a/src/config/config-parser.c ++++ b/src/config/config-parser.c +@@ -9,9 +9,9 @@ + #include "strescape.h" + #include "istream.h" + #include "module-dir.h" ++#include "version.h" + #include "settings.h" + #include "service-settings.h" +-#include "master-service.h" + #include "master-service-settings.h" + #include "all-settings.h" + #include "old-set-parser.h" +@@ -22,6 +22,7 @@ + + #include "default-settings-import.h" + ++#include + #include + #include + #include +diff --git a/src/config/old-set-parser.c b/src/config/old-set-parser.c +index 4ae8506dc7..ee2cb513c9 100644 +--- a/src/config/old-set-parser.c ++++ b/src/config/old-set-parser.c +@@ -2,7 +2,7 @@ + + #include "lib.h" + #include "array.h" +-#include "master-service.h" ++#include "version.h" + #include "settings-history.h" + #include "config-parser-private.h" + #include "old-set-parser.h" +diff --git a/src/doveadm/client-connection-tcp.c b/src/doveadm/client-connection-tcp.c +index 9299596f29..936bb03f01 100644 +--- a/src/doveadm/client-connection-tcp.c ++++ b/src/doveadm/client-connection-tcp.c +@@ -8,9 +8,9 @@ + #include "istream.h" + #include "ostream.h" + #include "strescape.h" ++#include "version.h" + #include "iostream-ssl.h" + #include "ostream-multiplex.h" +-#include "master-service.h" + #include "master-service-ssl.h" + #include "mail-storage-service.h" + #include "doveadm-util.h" +diff --git a/src/doveadm/doveadm-penalty.c b/src/doveadm/doveadm-penalty.c +index 8a098b3453..71a997e2e8 100644 +--- a/src/doveadm/doveadm-penalty.c ++++ b/src/doveadm/doveadm-penalty.c +@@ -7,7 +7,7 @@ + #include "hash.h" + #include "strescape.h" + #include "time-util.h" +-#include "master-service.h" ++#include "version.h" + #include "doveadm.h" + #include "doveadm-print.h" + +diff --git a/src/doveadm/doveadm-stats.c b/src/doveadm/doveadm-stats.c +index bd7552211a..87aef26a89 100644 +--- a/src/doveadm/doveadm-stats.c ++++ b/src/doveadm/doveadm-stats.c +@@ -6,7 +6,7 @@ + #include "str.h" + #include "strescape.h" + #include "write-full.h" +-#include "master-service.h" ++#include "version.h" + #include "doveadm.h" + #include "doveadm-print.h" + #include "stats-settings.h" +diff --git a/src/doveadm/doveadm-util.c b/src/doveadm/doveadm-util.c +index c21dbee6c0..4bea2437cc 100644 +--- a/src/doveadm/doveadm-util.c ++++ b/src/doveadm/doveadm-util.c +@@ -7,6 +7,7 @@ + #include "ostream.h" + #include "net.h" + #include "time-util.h" ++#include "version.h" + #include "master-service.h" + #include "module-dir.h" + #include "doveadm-settings.h" +diff --git a/src/doveadm/doveadm-who.c b/src/doveadm/doveadm-who.c +index ecdbb88bc7..29172afba0 100644 +--- a/src/doveadm/doveadm-who.c ++++ b/src/doveadm/doveadm-who.c +@@ -8,7 +8,7 @@ + #include "hash.h" + #include "str.h" + #include "strescape.h" +-#include "master-service.h" ++#include "version.h" + #include "doveadm.h" + #include "doveadm-print.h" + #include "doveadm-who.h" +diff --git a/src/doveadm/dsync/dsync-ibc-stream.c b/src/doveadm/dsync/dsync-ibc-stream.c +index 957c569cee..472d1fc517 100644 +--- a/src/doveadm/dsync/dsync-ibc-stream.c ++++ b/src/doveadm/dsync/dsync-ibc-stream.c +@@ -10,7 +10,7 @@ + #include "ostream.h" + #include "str.h" + #include "strescape.h" +-#include "master-service.h" ++#include "version.h" + #include "mail-cache.h" + #include "mail-storage-private.h" + #include "dsync-serializer.h" +diff --git a/src/imap-urlauth-login/imap-urlauth-login.c b/src/imap-urlauth-login/imap-urlauth-login.c +index 23a76f6c81..68b4aab159 100644 +--- a/src/imap-urlauth-login/imap-urlauth-login.c ++++ b/src/imap-urlauth-login/imap-urlauth-login.c +@@ -8,7 +8,7 @@ + #include "ioloop.h" + #include "istream.h" + #include "ostream.h" +-#include "master-service.h" ++#include "version.h" + #include "auth-client.h" + #include "client-common.h" + +diff --git a/src/imap/imap-client-hibernate.c b/src/imap/imap-client-hibernate.c +index ade5d250ea..067b984cc0 100644 +--- a/src/imap/imap-client-hibernate.c ++++ b/src/imap/imap-client-hibernate.c +@@ -9,7 +9,7 @@ + #include "base64.h" + #include "str.h" + #include "strescape.h" +-#include "master-service.h" ++#include "version.h" + #include "compression.h" + #include "mailbox-watch.h" + #include "imap-state.h" +diff --git a/src/lib-doveadm/doveadm-client.c b/src/lib-doveadm/doveadm-client.c +index 47b09f677f..6d570169ec 100644 +--- a/src/lib-doveadm/doveadm-client.c ++++ b/src/lib-doveadm/doveadm-client.c +@@ -10,8 +10,8 @@ + #include "ostream-dot.h" + #include "str.h" + #include "strescape.h" ++#include "version.h" + #include "iostream-ssl.h" +-#include "master-service.h" + #include "doveadm-protocol.h" + #include "doveadm-client.h" + #include "dns-lookup.h" +diff --git a/src/lib-master/Makefile.am b/src/lib-master/Makefile.am +index d6fe251e43..8939f759de 100644 +--- a/src/lib-master/Makefile.am ++++ b/src/lib-master/Makefile.am +@@ -47,7 +47,6 @@ pkginc_lib_HEADERS = $(headers) + + test_programs = \ + test-event-stats \ +- test-master-service \ + test-master-service-settings + + noinst_PROGRAMS = $(test_programs) +@@ -71,10 +70,6 @@ test_event_stats_SOURCES = test-event-stats.c + test_event_stats_LDADD = $(test_libs) + test_event_stats_DEPENDENCIES = $(test_deps) + +-test_master_service_SOURCES = test-master-service.c +-test_master_service_LDADD = $(test_libs) +-test_master_service_DEPENDENCIES = $(test_deps) +- + test_master_service_settings_SOURCES = test-master-service-settings.c + test_master_service_settings_LDADD = $(test_libs) + test_master_service_settings_DEPENDENCIES = $(test_deps) +diff --git a/src/lib-master/master-service-settings.c b/src/lib-master/master-service-settings.c +index 71438c0c4f..4194c19fab 100644 +--- a/src/lib-master/master-service-settings.c ++++ b/src/lib-master/master-service-settings.c +@@ -11,6 +11,7 @@ + #include "eacces-error.h" + #include "env-util.h" + #include "execv-const.h" ++#include "version.h" + #include "settings.h" + #include "stats-client.h" + #include "master-service-private.h" +diff --git a/src/lib-master/master-service.c b/src/lib-master/master-service.c +index a18cb39dbe..8f2ccacee1 100644 +--- a/src/lib-master/master-service.c ++++ b/src/lib-master/master-service.c +@@ -2035,91 +2035,6 @@ void master_status_update(struct master_service *service) + master_status_send(service, important_update); + } + +-bool version_string_verify(const char *line, const char *service_name, +- unsigned int major_version) +-{ +- unsigned int minor_version; +- +- return version_string_verify_full(line, service_name, +- major_version, &minor_version); +-} +- +-bool version_string_verify_full(const char *line, const char *service_name, +- unsigned int major_version, +- unsigned int *minor_version_r) +-{ +- size_t service_name_len = strlen(service_name); +- bool ret; +- +- if (!str_begins(line, "VERSION\t", &line)) +- return FALSE; +- +- if (strncmp(line, service_name, service_name_len) != 0 || +- line[service_name_len] != '\t') +- return FALSE; +- line += service_name_len + 1; +- +- T_BEGIN { +- const char *p = strchr(line, '\t'); +- +- if (p == NULL) +- ret = FALSE; +- else { +- ret = str_uint_equals(t_strdup_until(line, p), +- major_version); +- if (str_to_uint(p+1, minor_version_r) < 0) +- ret = FALSE; +- } +- } T_END; +- return ret; +-} +- +-int version_cmp(const char *version1, const char *version2) +-{ +- unsigned int v1, v2; +- +- do { +- if (str_parse_uint(version1, &v1, &version1) < 0) +- i_unreached(); +- if (str_parse_uint(version2, &v2, &version2) < 0) +- i_unreached(); +- if (*version1 == '.') +- version1++; +- else +- i_assert(*version1 == '\0'); +- if (*version2 == '.') +- version2++; +- else +- i_assert(*version2 == '\0'); +- +- if (v1 < v2) +- return -1; +- if (v1 > v2) +- return 1; +- } while (*version1 != '\0' && *version2 != '\0'); +- +- if (*version1 != '\0') +- return 1; +- if (*version2 != '\0') +- return -1; +- return 0; +-} +- +-bool version_is_valid(const char *version) +-{ +- unsigned int i; +- +- for (i = 0; version[i] != '\0'; i++) { +- if (version[i] == '.') { +- if (i == 0 || version[i-1] == '.' || +- version[i+1] == '\0') +- return FALSE; +- } else if (version[i] < '0' || version[i] > '9') +- return FALSE; +- } +- return i > 0; +-} +- + void master_service_set_process_shutdown_filter(struct master_service *service, + struct event_filter *filter) + { +diff --git a/src/lib-master/master-service.h b/src/lib-master/master-service.h +index 1dca3a6403..6c13ddaca7 100644 +--- a/src/lib-master/master-service.h ++++ b/src/lib-master/master-service.h +@@ -308,21 +308,6 @@ void master_service_deinit(struct master_service **service); + */ + void master_service_deinit_forked(struct master_service **_service); + +-/* Returns TRUE if line contains compatible service name and major version. +- The line is expected to be in format: +- VERSION service_name major version minor version */ +-bool version_string_verify(const char *line, const char *service_name, +- unsigned int major_version); +-/* Same as version_string_verify(), but return the minor version. */ +-bool version_string_verify_full(const char *line, const char *service_name, +- unsigned int major_version, +- unsigned int *minor_version_r); +-/* Compare number[.number[...]] style version numbers. Assert-crash if the +- version strings are invalid. */ +-int version_cmp(const char *version1, const char *version2); +-/* Returns TRUE if version string is a valid number[.number[...]] string. */ +-bool version_is_valid(const char *version); +- + /* Sets process shutdown filter */ + void master_service_set_process_shutdown_filter(struct master_service *service, + struct event_filter *filter); +diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am +index eb9b9ddda3..45a4f1160e 100644 +--- a/src/lib/Makefile.am ++++ b/src/lib/Makefile.am +@@ -197,6 +197,7 @@ liblib_la_SOURCES = \ + uri-util.c \ + utc-offset.c \ + utc-mktime.c \ ++ version.c \ + wildcard-match.c \ + write-full.c + +@@ -358,6 +359,7 @@ headers = \ + uri-util.h \ + utc-offset.h \ + utc-mktime.h \ ++ version.h \ + wildcard-match.h \ + write-full.h + +@@ -464,6 +466,7 @@ test_lib_SOURCES = \ + test-unichar.c \ + test-utc-mktime.c \ + test-uri.c \ ++ test-version.c \ + test-wildcard-match.c + + test_headers = \ +diff --git a/src/lib/test-lib.inc b/src/lib/test-lib.inc +index 2fceca2f9b..52259e4b96 100644 +--- a/src/lib/test-lib.inc ++++ b/src/lib/test-lib.inc +@@ -109,4 +109,5 @@ TEST(test_time_util) + TEST(test_unichar) + TEST(test_uri) + TEST(test_utc_mktime) ++TEST(test_version) + TEST(test_wildcard_match) +diff --git a/src/lib-master/test-master-service.c b/src/lib/test-version.c +similarity index 83% +rename from src/lib-master/test-master-service.c +rename to src/lib/test-version.c +index f30b250628..e938333f08 100644 +--- a/src/lib-master/test-master-service.c ++++ b/src/lib/test-version.c +@@ -1,7 +1,7 @@ +-/* Copyright (c) 2023 Dovecot authors, see the included COPYING file */ ++/* Copyright (c) 2023-2025 Dovecot authors, see the included COPYING file */ + +-#include "lib.h" +-#include "master-service.h" ++#include "test-lib.h" ++#include "version.h" + #include "test-common.h" + + static void test_version_is_valid(void) +@@ -56,12 +56,8 @@ static void test_version_cmp(void) + test_end(); + } + +-int main(void) ++void test_version(void) + { +- static void (*const test_functions[])(void) = { +- test_version_is_valid, +- test_version_cmp, +- NULL +- }; +- return test_run(test_functions); ++ test_version_is_valid(); ++ test_version_cmp(); + } +diff --git a/src/lib/version.c b/src/lib/version.c +new file mode 100644 +index 0000000000..f8bec420ab +--- /dev/null ++++ b/src/lib/version.c +@@ -0,0 +1,90 @@ ++/* Copyright (c) 2005-2025 Dovecot authors, see the included COPYING file */ ++ ++#include "lib.h" ++#include "version.h" ++ ++bool version_string_verify(const char *line, const char *service_name, ++ unsigned int major_version) ++{ ++ unsigned int minor_version; ++ ++ return version_string_verify_full(line, service_name, ++ major_version, &minor_version); ++} ++ ++bool version_string_verify_full(const char *line, const char *service_name, ++ unsigned int major_version, ++ unsigned int *minor_version_r) ++{ ++ size_t service_name_len = strlen(service_name); ++ bool ret; ++ ++ if (!str_begins(line, "VERSION\t", &line)) ++ return FALSE; ++ ++ if (strncmp(line, service_name, service_name_len) != 0 || ++ line[service_name_len] != '\t') ++ return FALSE; ++ line += service_name_len + 1; ++ ++ T_BEGIN { ++ const char *p = strchr(line, '\t'); ++ ++ if (p == NULL) ++ ret = FALSE; ++ else { ++ ret = str_uint_equals(t_strdup_until(line, p), ++ major_version); ++ if (str_to_uint(p+1, minor_version_r) < 0) ++ ret = FALSE; ++ } ++ } T_END; ++ return ret; ++} ++ ++int version_cmp(const char *version1, const char *version2) ++{ ++ unsigned int v1, v2; ++ ++ do { ++ if (str_parse_uint(version1, &v1, &version1) < 0) ++ i_unreached(); ++ if (str_parse_uint(version2, &v2, &version2) < 0) ++ i_unreached(); ++ if (*version1 == '.') ++ version1++; ++ else ++ i_assert(*version1 == '\0'); ++ if (*version2 == '.') ++ version2++; ++ else ++ i_assert(*version2 == '\0'); ++ ++ if (v1 < v2) ++ return -1; ++ if (v1 > v2) ++ return 1; ++ } while (*version1 != '\0' && *version2 != '\0'); ++ ++ if (*version1 != '\0') ++ return 1; ++ if (*version2 != '\0') ++ return -1; ++ return 0; ++} ++ ++bool version_is_valid(const char *version) ++{ ++ unsigned int i; ++ ++ for (i = 0; version[i] != '\0'; i++) { ++ if (version[i] == '.') { ++ if (i == 0 || version[i-1] == '.' || ++ version[i+1] == '\0') ++ return FALSE; ++ } else if (version[i] < '0' || version[i] > '9') ++ return FALSE; ++ } ++ return i > 0; ++} ++ +diff --git a/src/lib/version.h b/src/lib/version.h +new file mode 100644 +index 0000000000..f305f7f4e1 +--- /dev/null ++++ b/src/lib/version.h +@@ -0,0 +1,19 @@ ++#ifndef VERSION_H ++#define VERSION_H ++ ++/* Returns TRUE if line contains compatible service name and major version. ++ The line is expected to be in format: ++ VERSION service_name major version minor version */ ++bool version_string_verify(const char *line, const char *service_name, ++ unsigned int major_version); ++/* Same as version_string_verify(), but return the minor version. */ ++bool version_string_verify_full(const char *line, const char *service_name, ++ unsigned int major_version, ++ unsigned int *minor_version_r); ++/* Compare number[.number[...]] style version numbers. Assert-crash if the ++ version strings are invalid. */ ++int version_cmp(const char *version1, const char *version2); ++/* Returns TRUE if version string is a valid number[.number[...]] string. */ ++bool version_is_valid(const char *version); ++ ++#endif +diff --git a/src/util/script-login.c b/src/util/script-login.c +index ed356747dc..4df7f23174 100644 +--- a/src/util/script-login.c ++++ b/src/util/script-login.c +@@ -7,6 +7,7 @@ + #include "restrict-access.h" + #include "str.h" + #include "strescape.h" ++#include "version.h" + #include "settings-parser.h" + #include "mail-storage-service.h" + #include "master-interface.h" +diff --git a/src/util/script.c b/src/util/script.c +index 5838cdad31..391b87bc06 100644 +--- a/src/util/script.c ++++ b/src/util/script.c +@@ -8,6 +8,7 @@ + #include "execv-const.h" + #include "write-full.h" + #include "restrict-access.h" ++#include "version.h" + #include "master-interface.h" + #include "master-service.h" + +-- +2.47.3 + diff --git a/patches/0011-lib-mail-Introduce-struct-message_part_data_limits-a.patch b/patches/0011-lib-mail-Introduce-struct-message_part_data_limits-a.patch new file mode 100644 index 0000000..f5da104 --- /dev/null +++ b/patches/0011-lib-mail-Introduce-struct-message_part_data_limits-a.patch @@ -0,0 +1,323 @@ +From 3e8ffe61ee5719c05e68d9be7f913dc3cd2a0e6c Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 17 Apr 2026 15:54:48 +0200 +Subject: [PATCH 11/14] lib-mail: Introduce struct message_part_data_limits and + thread it through parsers + +Add an empty struct message_part_data_limits and pass it by pointer through +message_part_data_parse_from_header() and message_part_envelope_parse_from_header(). + +No behaviour change: the struct has no fields yet and no limits are applied. +Subsequent commits add the individual limit fields and enforcement. + +Co-Authored-By: Claude Sonnet 4.6 +--- + src/lib-imap/test-imap-bodystructure.c | 3 ++- + src/lib-imap/test-imap-envelope.c | 4 +++- + src/lib-mail/message-part-data.c | 5 ++++- + src/lib-mail/message-part-data.h | 7 +++++++ + src/lib-mail/test-message-decoder.c | 16 ++++++++++++---- + src/lib-mail/test-message-parser.c | 3 ++- + src/lib-mail/test-message-part-data.c | 3 ++- + src/lib-storage/index/index-mail-headers.c | 10 +++++++--- + src/lib-storage/index/index-mail.c | 21 +++++++++++++++++---- + src/lib-storage/index/index-mail.h | 2 ++ + 10 files changed, 58 insertions(+), 16 deletions(-) + +diff --git a/src/lib-imap/test-imap-bodystructure.c b/src/lib-imap/test-imap-bodystructure.c +index 40c8a8c6ec..abe35ac6f3 100644 +--- a/src/lib-imap/test-imap-bodystructure.c ++++ b/src/lib-imap/test-imap-bodystructure.c +@@ -398,10 +398,11 @@ msg_parse(pool_t pool, const char *message, unsigned int max_nested_mime_parts, + + input = i_stream_create_from_data(message, strlen(message)); + parser = message_parser_init(pool, input, &parser_set); ++ struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT; + while ((ret = message_parser_parse_next_block(parser, &block)) > 0) { + if (parse_bodystructure) { + message_part_data_parse_from_header(pool, block.part, +- block.hdr); ++ &limits, block.hdr); + } + } + test_assert(ret < 0); +diff --git a/src/lib-imap/test-imap-envelope.c b/src/lib-imap/test-imap-envelope.c +index c9b92b4be2..498c6ac9f4 100644 +--- a/src/lib-imap/test-imap-envelope.c ++++ b/src/lib-imap/test-imap-envelope.c +@@ -132,9 +132,11 @@ msg_parse(pool_t pool, const char *message) + + input = i_stream_create_from_data(message, strlen(message)); + parser = message_parser_init(pool, input, &parser_set); ++ struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT; + while ((ret = message_parser_parse_next_block(parser, &block)) > 0) { + i_assert(block.part->parent == NULL); +- message_part_envelope_parse_from_header(pool, &envlp, block.hdr); ++ message_part_envelope_parse_from_header(pool, &envlp, &limits, ++ block.hdr); + } + test_assert(ret < 0); + +diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c +index acd50e5210..c302fe568b 100644 +--- a/src/lib-mail/message-part-data.c ++++ b/src/lib-mail/message-part-data.c +@@ -173,6 +173,7 @@ envelope_get_field(const char *name) + + void message_part_envelope_parse_from_header(pool_t pool, + struct message_part_envelope **data, ++ struct message_part_data_limits *limits ATTR_UNUSED, + struct message_header_line *hdr) + { + struct message_part_envelope *d; +@@ -480,6 +481,7 @@ parse_content_header(struct message_part_data *data, + + void message_part_data_parse_from_header(pool_t pool, + struct message_part *part, ++ struct message_part_data_limits *limits, + struct message_header_line *hdr) + { + struct message_part_data *part_data; +@@ -525,7 +527,8 @@ void message_part_data_parse_from_header(pool_t pool, + + if (parent_rfc822) { + /* message/rfc822, we need the envelope */ +- message_part_envelope_parse_from_header(pool, &part_data->envelope, hdr); ++ message_part_envelope_parse_from_header(pool, &part_data->envelope, ++ limits, hdr); + } + } + +diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h +index 9513bfc2f0..1514f0daf2 100644 +--- a/src/lib-mail/message-part-data.h ++++ b/src/lib-mail/message-part-data.h +@@ -8,6 +8,11 @@ + + struct message_header_line; + ++struct message_part_data_limits { ++}; ++ ++#define MESSAGE_PART_DATA_LIMITS_INIT { } ++ + struct message_part_param { + const char *name; + const char *value; +@@ -91,11 +96,13 @@ bool message_part_is_attachment(struct message_part *part, + /* Update envelope data based from given header field */ + void message_part_envelope_parse_from_header(pool_t pool, + struct message_part_envelope **_data, ++ struct message_part_data_limits *limits, + struct message_header_line *hdr); + + /* Parse a single header. Note that this modifies part->context. */ + void message_part_data_parse_from_header(pool_t pool, + struct message_part *part, ++ struct message_part_data_limits *limits, + struct message_header_line *hdr); + + #endif +diff --git a/src/lib-mail/test-message-decoder.c b/src/lib-mail/test-message-decoder.c +index 83c267187b..ad212c8546 100644 +--- a/src/lib-mail/test-message-decoder.c ++++ b/src/lib-mail/test-message-decoder.c +@@ -260,8 +260,10 @@ static void test_message_decoder_content_transfer_encoding(void) + parser = message_parser_init(pool, istream, &parser_set); + decoder = message_decoder_init(NULL, 0); + ++ struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT; + while ((ret = message_parser_parse_next_block(parser, &input)) > 0) { +- message_part_data_parse_from_header(pool, input.part, input.hdr); ++ message_part_data_parse_from_header(pool, input.part, &limits, ++ input.hdr); + if (message_decoder_decode_next_block(decoder, &input, &output) && + output.hdr == NULL && output.size > 0) + str_append_data(str_out, output.data, output.size); +@@ -349,8 +351,10 @@ static void test_message_decoder_invalid_content_transfer_encoding(void) + parser = message_parser_init(pool, istream, &parser_set); + decoder = message_decoder_init(NULL, 0); + ++ struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT; + while ((ret = message_parser_parse_next_block(parser, &input)) > 0) { +- message_part_data_parse_from_header(pool, input.part, input.hdr); ++ message_part_data_parse_from_header(pool, input.part, &limits, ++ input.hdr); + if (input.hdr != NULL && + strcasecmp(input.hdr->name, "content-transfer-encoding") == 0) { + enum message_cte cte = message_decoder_parse_cte(input.hdr); +@@ -480,8 +484,10 @@ UNICODE_REPLACEMENT_CHAR_UTF8; + parser = message_parser_init(pool, istream, &parser_set); + decoder = message_decoder_init(NULL, 0); + ++ struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT; + while ((ret = message_parser_parse_next_block(parser, &input)) > 0) { +- message_part_data_parse_from_header(pool, input.part, input.hdr); ++ message_part_data_parse_from_header(pool, input.part, &limits, ++ input.hdr); + if (message_decoder_decode_next_block(decoder, &input, &output) && + output.hdr == NULL && output.size > 0) + str_append_data(str_out, output.data, output.size); +@@ -531,8 +537,10 @@ static void test_message_decoder_charset_mime_part_change(void) + parser = message_parser_init(pool, istream, &parser_set); + decoder = message_decoder_init(NULL, 0); + ++ struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT; + while ((ret = message_parser_parse_next_block(parser, &input)) > 0) { +- message_part_data_parse_from_header(pool, input.part, input.hdr); ++ message_part_data_parse_from_header(pool, input.part, &limits, ++ input.hdr); + if (message_decoder_decode_next_block(decoder, &input, &output) && + output.hdr == NULL && output.size > 0) + str_append_data(str_out, output.data, output.size); +diff --git a/src/lib-mail/test-message-parser.c b/src/lib-mail/test-message-parser.c +index 3b94034c09..b66e0a664e 100644 +--- a/src/lib-mail/test-message-parser.c ++++ b/src/lib-mail/test-message-parser.c +@@ -51,12 +51,13 @@ static int message_parse_stream(pool_t pool, struct istream *input, + struct message_parser_ctx *parser; + struct message_block block; + ++ struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT; + i_zero(&block); + parser = message_parser_init(pool, input, set); + while ((ret = message_parser_parse_next_block(parser, &block)) > 0) + if (parse_data) + message_part_data_parse_from_header(pool, block.part, +- block.hdr); ++ &limits, block.hdr); + message_parser_deinit(&parser, parts_r); + test_assert(input->stream_errno == 0); + return ret; +diff --git a/src/lib-mail/test-message-part-data.c b/src/lib-mail/test-message-part-data.c +index 470d739b5e..63228d1b3d 100644 +--- a/src/lib-mail/test-message-part-data.c ++++ b/src/lib-mail/test-message-part-data.c +@@ -14,12 +14,13 @@ static int message_parse_stream(pool_t pool, struct istream *input, + struct message_parser_ctx *parser; + struct message_block block; + ++ struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT; + i_zero(&block); + parser = message_parser_init(pool, input, set); + while ((ret = message_parser_parse_next_block(parser, &block)) > 0) + if (parse_data) + message_part_data_parse_from_header(pool, block.part, +- block.hdr); ++ &limits, block.hdr); + message_parser_deinit(&parser, parts_r); + test_assert(input->stream_errno == 0); + return ret; +diff --git a/src/lib-storage/index/index-mail-headers.c b/src/lib-storage/index/index-mail-headers.c +index a9f4d55c0d..9d608acc61 100644 +--- a/src/lib-storage/index/index-mail-headers.c ++++ b/src/lib-storage/index/index-mail-headers.c +@@ -308,12 +308,15 @@ void index_mail_parse_header(struct message_part *part, + if (data->save_bodystructure_header && + !data->parsed_bodystructure_header) { + i_assert(part != NULL); +- message_part_data_parse_from_header(mail->mail.data_pool, part, hdr); ++ message_part_data_parse_from_header(mail->mail.data_pool, part, ++ &mail->data.part_data_limits, ++ hdr); + } + + if (data->save_envelope) { + message_part_envelope_parse_from_header(mail->mail.data_pool, +- &data->envelope_data, hdr); ++ &data->envelope_data, ++ &mail->data.part_data_limits, hdr); + + if (hdr == NULL) + index_mail_parse_finish_imap_envelope(mail); +@@ -510,7 +513,8 @@ imap_envelope_parse_callback(struct message_header_line *hdr, + struct index_mail *mail) + { + message_part_envelope_parse_from_header(mail->mail.data_pool, +- &mail->data.envelope_data, hdr); ++ &mail->data.envelope_data, ++ &mail->data.part_data_limits, hdr); + + if (hdr == NULL) + index_mail_parse_finish_imap_envelope(mail); +diff --git a/src/lib-storage/index/index-mail.c b/src/lib-storage/index/index-mail.c +index 70060ba4f7..3911f6db1c 100644 +--- a/src/lib-storage/index/index-mail.c ++++ b/src/lib-storage/index/index-mail.c +@@ -696,11 +696,16 @@ void index_mail_cache_pop3_data(struct mail *_mail, + &order, sizeof(order)); + } + ++struct parse_bodystructure_ctx { ++ pool_t pool; ++ struct message_part_data_limits *limits; ++}; ++ + static void parse_bodystructure_part_header(struct message_part *part, + struct message_header_line *hdr, +- pool_t pool) ++ struct parse_bodystructure_ctx *ctx) + { +- message_part_data_parse_from_header(pool, part, hdr); ++ message_part_data_parse_from_header(ctx->pool, part, ctx->limits, hdr); + } + + static bool want_plain_bodystructure_cached(struct index_mail *mail) +@@ -1317,9 +1322,13 @@ static int index_mail_parse_body(struct index_mail *mail, + /* bodystructure header is parsed, we want the body's mime + headers too */ + i_assert(data->parsed_bodystructure_header); ++ struct parse_bodystructure_ctx bsctx = { ++ .pool = mail->mail.data_pool, ++ .limits = &mail->data.part_data_limits, ++ }; + message_parser_parse_body(data->parser_ctx, + parse_bodystructure_part_header, +- mail->mail.data_pool); ++ &bsctx); + } else { + message_parser_parse_body(data->parser_ctx, + *null_message_part_header_callback, NULL); +@@ -1860,6 +1869,8 @@ static void index_mail_init_data(struct index_mail *mail) + data->received_date = (time_t)-1; + data->sent_date.time = (uint32_t)-1; + data->dont_cache_field_idx = UINT_MAX; ++ data->part_data_limits = (struct message_part_data_limits) ++ MESSAGE_PART_DATA_LIMITS_INIT; + + data->wanted_fields = mail->mail.wanted_fields; + if (mail->mail.wanted_headers != NULL) { +@@ -2321,7 +2332,9 @@ void index_mail_cache_parse_continue(struct mail *_mail) + mail->data.header_parsed = TRUE; + } else { + message_part_data_parse_from_header(mail->mail.data_pool, +- block.part, block.hdr); ++ block.part, ++ &mail->data.part_data_limits, ++ block.hdr); + } + } + } +diff --git a/src/lib-storage/index/index-mail.h b/src/lib-storage/index/index-mail.h +index 9e2159898e..d1625b3f6e 100644 +--- a/src/lib-storage/index/index-mail.h ++++ b/src/lib-storage/index/index-mail.h +@@ -2,6 +2,7 @@ + #define INDEX_MAIL_H + + #include "message-size.h" ++#include "message-part-data.h" + #include "mail-cache.h" + #include "mail-storage-private.h" + +@@ -84,6 +85,7 @@ struct index_mail_data { + const char *envelope, *body, *bodystructure, *guid, *filename; + const char *from_envelope, *body_snippet; + struct message_part_envelope *envelope_data; ++ struct message_part_data_limits part_data_limits; + + uint32_t cache_flags; + uint64_t modseq, pvt_modseq; +-- +2.47.3 + diff --git a/patches/0012-config-doveconf-dF-Add-dovecot_storage_version.patch b/patches/0012-config-doveconf-dF-Add-dovecot_storage_version.patch new file mode 100644 index 0000000..1cae4f2 --- /dev/null +++ b/patches/0012-config-doveconf-dF-Add-dovecot_storage_version.patch @@ -0,0 +1,63 @@ +From ba5efe2541fa89e9cfe4e7fae4526e0f5dc18a9c Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 9 Apr 2025 14:11:42 +0300 +Subject: [PATCH 12/12] config: doveconf -dF - Add dovecot_storage_version + +--- + src/config/config-parser.c | 14 ++++++++++++++ + src/config/config-parser.h | 3 +++ + src/config/doveconf.c | 2 ++ + 3 files changed, 19 insertions(+) + +Index: trixie/src/config/config-parser.c +=================================================================== +--- trixie.orig/src/config/config-parser.c ++++ trixie/src/config/config-parser.c +@@ -2958,6 +2958,20 @@ int config_parse_file(const char *path, + hook_config_parser_begin(&ctx); + } T_END; + ++ if ((flags & CONFIG_PARSE_FLAG_DEFAULT_VERSION) != 0) { ++ /* Use default settings. Set dovecot_storage_version to the ++ latest version, so it won't cause a failure. ++ ++ When building from git we don't know the latest version, so ++ just use 9999. The version validity checks are disabled for ++ git builds, so this should work. */ ++ const char *version = version_is_valid(DOVECOT_VERSION) ? ++ DOVECOT_VERSION : "9999"; ++ if (config_apply_exact_line(&ctx, NULL, "dovecot_storage_version", ++ version) < 0) ++ i_panic("Couldn't set default dovecot_storage_version: %s", ctx.error); ++ } ++ + internal.path = "Internal config_import"; + ctx.cur_input->input = config_import == NULL ? + i_stream_create_from_data("", 0) : +Index: trixie/src/config/config-parser.h +=================================================================== +--- trixie.orig/src/config/config-parser.h ++++ trixie/src/config/config-parser.h +@@ -40,6 +40,9 @@ enum config_parse_flags { + CONFIG_PARSE_FLAG_MERGE_DEFAULT_FILTERS = BIT(9), + /* Ignore unknown settings in the config file. */ + CONFIG_PARSE_FLAG_IGNORE_UNKNOWN = BIT(10), ++ /* Executing "doveconf -dF" - add default dovecot_storage_version ++ setting. */ ++ CONFIG_PARSE_FLAG_DEFAULT_VERSION = BIT(11), + }; + + /* Used to track changed settings for a setting_parser_info. Initially only +Index: trixie/src/config/doveconf.c +=================================================================== +--- trixie.orig/src/config/doveconf.c ++++ trixie/src/config/doveconf.c +@@ -1234,6 +1234,8 @@ int main(int argc, char *argv[]) + flags |= CONFIG_PARSE_FLAG_DELAY_ERRORS; + if (exec_args == NULL) + flags |= CONFIG_PARSE_FLAG_EXTERNAL_HOOKS; ++ else if (dump_defaults) ++ flags |= CONFIG_PARSE_FLAG_DEFAULT_VERSION; + T_BEGIN { + ret = config_parse_file(dump_defaults ? NULL : config_path, + flags, have_dump_filter ? &dump_filter : NULL, diff --git a/patches/0012-lib-mail-Limit-total-address-count-per-message-to-10.patch b/patches/0012-lib-mail-Limit-total-address-count-per-message-to-10.patch new file mode 100644 index 0000000..dce45f3 --- /dev/null +++ b/patches/0012-lib-mail-Limit-total-address-count-per-message-to-10.patch @@ -0,0 +1,71 @@ +From f42717983640b9df13ce3214cec311102d9dcb6b Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 17 Apr 2026 15:55:48 +0200 +Subject: [PATCH 12/14] lib-mail: Limit total address count per message to 100 + 000 + +A message with millions of addresses across all its envelope headers can +exhaust memory when parsed (each struct message_address is ~100 bytes +regardless of whether the raw address is only a few bytes long). + +Add remaining_addresses to struct message_part_data_limits, initialised to +MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES (100 000). Pass the remaining budget +as max_addresses to message_address_parse_full() so parsing stops at the +limit, then deduct the actual count parsed from the budget. + +Co-Authored-By: Claude Sonnet 4.6 +--- + src/lib-mail/message-part-data.c | 7 +++++-- + src/lib-mail/message-part-data.h | 6 +++++- + 2 files changed, 10 insertions(+), 3 deletions(-) + +diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c +index c302fe568b..24e1a31964 100644 +--- a/src/lib-mail/message-part-data.c ++++ b/src/lib-mail/message-part-data.c +@@ -173,7 +173,7 @@ envelope_get_field(const char *name) + + void message_part_envelope_parse_from_header(pool_t pool, + struct message_part_envelope **data, +- struct message_part_data_limits *limits ATTR_UNUSED, ++ struct message_part_data_limits *limits, + struct message_header_line *hdr) + { + struct message_part_envelope *d; +@@ -238,9 +238,12 @@ void message_part_envelope_parse_from_header(pool_t pool, + if (addr_p != NULL) { + message_address_parse_full(pool, hdr->full_value, + hdr->full_value_len, +- UINT_MAX, ++ limits->remaining_addresses, + MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING, + &new_addr); ++ i_assert(new_addr.count <= limits->remaining_addresses); ++ limits->remaining_addresses -= new_addr.count; ++ + /* Merge multiple headers the same as if they were comma + separated in a single line. This is better from security + point of view, because attacker could intentionally write +diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h +index 1514f0daf2..af09ea715e 100644 +--- a/src/lib-mail/message-part-data.h ++++ b/src/lib-mail/message-part-data.h +@@ -8,10 +8,14 @@ + + struct message_header_line; + ++#define MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES 100000 ++ + struct message_part_data_limits { ++ unsigned int remaining_addresses; + }; + +-#define MESSAGE_PART_DATA_LIMITS_INIT { } ++#define MESSAGE_PART_DATA_LIMITS_INIT \ ++ { MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES } + + struct message_part_param { + const char *name; +-- +2.47.3 + diff --git a/patches/0013-lib-mail-Limit-total-Content-Language-tag-count-per-.patch b/patches/0013-lib-mail-Limit-total-Content-Language-tag-count-per-.patch new file mode 100644 index 0000000..81d39bf --- /dev/null +++ b/patches/0013-lib-mail-Limit-total-Content-Language-tag-count-per-.patch @@ -0,0 +1,99 @@ +From 5292d2277474680902437ec2a23c684041cdab61 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 17 Apr 2026 15:56:37 +0200 +Subject: [PATCH 13/14] lib-mail: Limit total Content-Language tag count per + message to 100 000 + +A multipart message with many MIME parts each containing many language tags +can exhaust memory: the per-part RFC 2231 parser had no cumulative limit. + +Add remaining_language_tags to struct message_part_data_limits, initialised +to MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS (100 000). Break out of the +tag-parsing loop in parse_content_language() once the budget reaches zero. + +Co-Authored-By: Claude Sonnet 4.6 +--- + src/lib-mail/message-part-data.c | 15 ++++++++++----- + src/lib-mail/message-part-data.h | 5 ++++- + 2 files changed, 14 insertions(+), 6 deletions(-) + +diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c +index 24e1a31964..2eed994e45 100644 +--- a/src/lib-mail/message-part-data.c ++++ b/src/lib-mail/message-part-data.c +@@ -380,7 +380,8 @@ parse_content_disposition(struct message_part_data *data, + + static void + parse_content_language(struct message_part_data *data, +- pool_t pool, const unsigned char *value, size_t value_len) ++ pool_t pool, const unsigned char *value, size_t value_len, ++ struct message_part_data_limits *limits) + { + struct rfc822_parser_context parser; + ARRAY_TYPE(const_string) langs; +@@ -398,8 +399,11 @@ parse_content_language(struct message_part_data *data, + + rfc822_skip_lwsp(&parser); + while (rfc822_parse_atom(&parser, str) >= 0) { +- const char *lang = p_strdup(pool, str_c(str)); ++ if (limits->remaining_language_tags == 0) ++ break; ++ limits->remaining_language_tags--; + ++ const char *lang = p_strdup(pool, str_c(str)); + array_push_back(&langs, &lang); + str_truncate(str, 0); + +@@ -419,7 +423,8 @@ parse_content_language(struct message_part_data *data, + + static void + parse_content_header(struct message_part_data *data, +- pool_t pool, struct message_header_line *hdr) ++ pool_t pool, struct message_header_line *hdr, ++ struct message_part_data_limits *limits) + { + const char *name = hdr->name + strlen("Content-"); + +@@ -459,7 +464,7 @@ parse_content_header(struct message_part_data *data, + if (strcasecmp(name, "Language") == 0 && + data->content_language == NULL) { + parse_content_language(data, pool, +- hdr->full_value, hdr->full_value_len); ++ hdr->full_value, hdr->full_value_len, limits); + } else if (strcasecmp(name, "Location") == 0 && + data->content_location == NULL) { + data->content_location = +@@ -524,7 +529,7 @@ void message_part_data_parse_from_header(pool_t pool, + + if (str_begins_icase_with(hdr->name, "Content-")) { + T_BEGIN { +- parse_content_header(part_data, pool, hdr); ++ parse_content_header(part_data, pool, hdr, limits); + } T_END; + } + +diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h +index af09ea715e..7f1f9c3e1f 100644 +--- a/src/lib-mail/message-part-data.h ++++ b/src/lib-mail/message-part-data.h +@@ -9,13 +9,16 @@ + struct message_header_line; + + #define MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES 100000 ++#define MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS 100000 + + struct message_part_data_limits { + unsigned int remaining_addresses; ++ unsigned int remaining_language_tags; + }; + + #define MESSAGE_PART_DATA_LIMITS_INIT \ +- { MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES } ++ { MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES, \ ++ MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS } + + struct message_part_param { + const char *name; +-- +2.47.3 + diff --git a/patches/0014-lib-mail-Limit-total-MIME-parameter-count-per-messag.patch b/patches/0014-lib-mail-Limit-total-MIME-parameter-count-per-messag.patch new file mode 100644 index 0000000..6279ab5 --- /dev/null +++ b/patches/0014-lib-mail-Limit-total-MIME-parameter-count-per-messag.patch @@ -0,0 +1,130 @@ +From 6a71e37c3edae89cd446a1bb8b1f7137b5a25754 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 17 Apr 2026 15:57:24 +0200 +Subject: [PATCH 14/14] lib-mail: Limit total MIME parameter count per message + to 200 000 + +Content-Type and Content-Disposition parameters are both parsed through +parse_mime_parameters(). A message with many MIME parts each having many +parameters can accumulate millions of struct message_part_param entries. + +Add remaining_mime_params to struct message_part_data_limits (combined +budget for both Content-Type and Content-Disposition), initialised to +MESSAGE_PART_DATA_MAX_TOTAL_MIME_PARAMS (200 000). Cap params_count to the +remaining budget before allocating and deduct accordingly. + +Co-Authored-By: Claude Sonnet 4.6 +--- + src/lib-mail/message-part-data.c | 21 ++++++++++++++------- + src/lib-mail/message-part-data.h | 6 +++++- + 2 files changed, 19 insertions(+), 8 deletions(-) + +diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c +index 2eed994e45..e0a3676aae 100644 +--- a/src/lib-mail/message-part-data.c ++++ b/src/lib-mail/message-part-data.c +@@ -263,7 +263,8 @@ void message_part_envelope_parse_from_header(pool_t pool, + static void + parse_mime_parameters(struct rfc822_parser_context *parser, + pool_t pool, const struct message_part_param **params_r, +- unsigned int *params_count_r) ++ unsigned int *params_count_r, ++ struct message_part_data_limits *limits) + { + const char *const *results; + struct message_part_param *params; +@@ -275,6 +276,10 @@ parse_mime_parameters(struct rfc822_parser_context *parser, + i_assert((params_count % 2) == 0); + params_count /= 2; + ++ if (params_count > limits->remaining_mime_params) ++ params_count = limits->remaining_mime_params; ++ limits->remaining_mime_params -= params_count; ++ + if (params_count > 0) { + params = p_new(pool, struct message_part_param, params_count); + for (i = 0; i < params_count; i++) { +@@ -289,7 +294,8 @@ parse_mime_parameters(struct rfc822_parser_context *parser, + + static void + parse_content_type(struct message_part_data *data, +- pool_t pool, struct message_header_line *hdr) ++ pool_t pool, struct message_header_line *hdr, ++ struct message_part_data_limits *limits) + { + struct rfc822_parser_context parser; + string_t *str; +@@ -332,7 +338,7 @@ parse_content_type(struct message_part_data *data, + + parse_mime_parameters(&parser, pool, + &data->content_type_params, +- &data->content_type_params_count); ++ &data->content_type_params_count, limits); + rfc822_parser_deinit(&parser); + } + +@@ -357,7 +363,8 @@ parse_content_transfer_encoding(struct message_part_data *data, + + static void + parse_content_disposition(struct message_part_data *data, +- pool_t pool, struct message_header_line *hdr) ++ pool_t pool, struct message_header_line *hdr, ++ struct message_part_data_limits *limits) + { + struct rfc822_parser_context parser; + string_t *str; +@@ -374,7 +381,7 @@ parse_content_disposition(struct message_part_data *data, + + parse_mime_parameters(&parser, pool, + &data->content_disposition_params, +- &data->content_disposition_params_count); ++ &data->content_disposition_params_count, limits); + rfc822_parser_deinit(&parser); + } + +@@ -453,7 +460,7 @@ parse_content_header(struct message_part_data *data, + case 't': + case 'T': + if (strcasecmp(name, "Type") == 0 && data->content_type == NULL) +- parse_content_type(data, pool, hdr); ++ parse_content_type(data, pool, hdr, limits); + else if (strcasecmp(name, "Transfer-Encoding") == 0 && + data->content_transfer_encoding == NULL) + parse_content_transfer_encoding(data, pool, hdr); +@@ -482,7 +489,7 @@ parse_content_header(struct message_part_data *data, + hdr->full_value_len); + else if (strcasecmp(name, "Disposition") == 0 && + data->content_disposition_params == NULL) +- parse_content_disposition(data, pool, hdr); ++ parse_content_disposition(data, pool, hdr, limits); + break; + } + } +diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h +index 7f1f9c3e1f..f439926c90 100644 +--- a/src/lib-mail/message-part-data.h ++++ b/src/lib-mail/message-part-data.h +@@ -10,15 +10,19 @@ struct message_header_line; + + #define MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES 100000 + #define MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS 100000 ++/* Combined limit for Content-Type + Content-Disposition parameters */ ++#define MESSAGE_PART_DATA_MAX_TOTAL_MIME_PARAMS 200000 + + struct message_part_data_limits { + unsigned int remaining_addresses; + unsigned int remaining_language_tags; ++ unsigned int remaining_mime_params; + }; + + #define MESSAGE_PART_DATA_LIMITS_INIT \ + { MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES, \ +- MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS } ++ MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS, \ ++ MESSAGE_PART_DATA_MAX_TOTAL_MIME_PARAMS } + + struct message_part_param { + const char *name; +-- +2.47.3 + diff --git a/patches/CVE-2025-59028.patch b/patches/CVE-2025-59028.patch new file mode 100644 index 0000000..cfa08c6 --- /dev/null +++ b/patches/CVE-2025-59028.patch @@ -0,0 +1,40 @@ +From dae1a1b84115b2a6146afd9a51e17137264caa0d Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 4 Nov 2025 11:34:30 +0200 +Subject: [PATCH 01/24] auth: Don't disconnect auth client when invalid base64 + SASL input is received + +The base64 input comes from untrusted client. It shouldn't cause the auth +client to disconnect, which causes other concurrent logins to be aborted. + +Broken by 1486c30e191ff079bfa78e7950173bb33d8073d9 +--- + src/auth/auth-request-handler.c | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +Index: dovecot/src/auth/auth-request-handler.c +=================================================================== +--- dovecot.orig/src/auth/auth-request-handler.c ++++ dovecot/src/auth/auth-request-handler.c +@@ -715,6 +715,9 @@ int auth_request_handler_auth_begin(stru + auth_request_handler_auth_fail_code(handler, request, + AUTH_CLIENT_FAIL_CODE_INVALID_BASE64, + "Invalid base64 data in initial response"); ++ /* The base64 input came from untrusted client. It's ++ an expected auth failure, so don't disconnect the ++ auth client. */ + return 1; + } + initial_resp_data = +@@ -787,7 +790,10 @@ int auth_request_handler_auth_continue(s + auth_request_handler_auth_fail_code(handler, request, + AUTH_CLIENT_FAIL_CODE_INVALID_BASE64, + "Invalid base64 data in continued response"); +- return -1; ++ /* The base64 input came from untrusted client. It's ++ an expected auth failure, so don't disconnect the ++ auth client. */ ++ return 1; + } + } + diff --git a/patches/CVE-2025-59031.patch b/patches/CVE-2025-59031.patch new file mode 100644 index 0000000..ea8cf1d --- /dev/null +++ b/patches/CVE-2025-59031.patch @@ -0,0 +1,137 @@ +From 089edc7750160bf224011c015347db1bdea435e3 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Thu, 8 Jan 2026 08:51:59 +0200 +Subject: [PATCH 02/24] fts: Remove decode2text.sh + +The script is flawed and not fit for production use, should +recommend writing your own script, or using Apache Tika. +--- + src/plugins/fts/Makefile.am | 3 - + src/plugins/fts/decode2text.sh | 105 --------------------------------- + 2 files changed, 108 deletions(-) + delete mode 100755 src/plugins/fts/decode2text.sh + +Index: dovecot/src/plugins/fts/Makefile.am +=================================================================== +--- dovecot.orig/src/plugins/fts/Makefile.am ++++ dovecot/src/plugins/fts/Makefile.am +@@ -65,9 +65,6 @@ xml2text_CPPFLAGS = $(AM_CPPFLAGS) $(BIN + xml2text_LDADD = $(LIBDOVECOT) $(BINARY_LDFLAGS) + xml2text_DEPENDENCIES = $(module_LTLIBRARIES) $(LIBDOVECOT_DEPS) + +-pkglibexec_SCRIPTS = decode2text.sh +-EXTRA_DIST = $(pkglibexec_SCRIPTS) +- + doveadm_module_LTLIBRARIES = \ + lib20_doveadm_fts_plugin.la + +Index: dovecot/src/plugins/fts/decode2text.sh +=================================================================== +--- dovecot.orig/src/plugins/fts/decode2text.sh ++++ /dev/null +@@ -1,105 +0,0 @@ +-#!/bin/sh +- +-# Example attachment decoder script. The attachment comes from stdin, and +-# the script is expected to output UTF-8 data to stdout. (If the output isn't +-# UTF-8, everything except valid UTF-8 sequences are dropped from it.) +- +-# The attachment decoding is enabled by setting: +-# +-# plugin { +-# fts_decoder = decode2text +-# } +-# service decode2text { +-# executable = script /usr/local/libexec/dovecot/decode2text.sh +-# user = dovecot +-# unix_listener decode2text { +-# mode = 0666 +-# } +-# } +- +-libexec_dir=`dirname $0` +-content_type=$1 +- +-# The second parameter is the format's filename extension, which is used when +-# found from a filename of application/octet-stream. You can also add more +-# extensions by giving more parameters. +-formats='application/pdf pdf +-application/x-pdf pdf +-application/msword doc +-application/mspowerpoint ppt +-application/vnd.ms-powerpoint ppt +-application/ms-excel xls +-application/x-msexcel xls +-application/vnd.ms-excel xls +-application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +-application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +-application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +-application/vnd.oasis.opendocument.text odt +-application/vnd.oasis.opendocument.spreadsheet ods +-application/vnd.oasis.opendocument.presentation odp +-' +- +-if [ "$content_type" = "" ]; then +- echo "$formats" +- exit 0 +-fi +- +-fmt=`echo "$formats" | grep -w "^$content_type" | cut -d ' ' -f 2` +-if [ "$fmt" = "" ]; then +- echo "Content-Type: $content_type not supported" >&2 +- exit 1 +-fi +- +-# most decoders can't handle stdin directly, so write the attachment +-# to a temp file +-path=`mktemp` +-trap "rm -f $path" 0 1 2 3 14 15 +-cat > $path +- +-xmlunzip() { +- name=$1 +- +- tempdir=`mktemp -d` +- if [ "$tempdir" = "" ]; then +- exit 1 +- fi +- trap "rm -rf $path $tempdir" 0 1 2 3 14 15 +- cd $tempdir || exit 1 +- unzip -q "$path" 2>/dev/null || exit 0 +- find . -name "$name" -print0 | xargs -0 cat | +- $libexec_dir/xml2text +-} +- +-wait_timeout() { +- childpid=$! +- trap "kill -9 $childpid; rm -f $path" 1 2 3 14 15 +- wait $childpid +-} +- +-LANG=en_US.UTF-8 +-export LANG +-if [ $fmt = "pdf" ]; then +- /usr/bin/pdftotext $path - 2>/dev/null& +- wait_timeout 2>/dev/null +-elif [ $fmt = "doc" ]; then +- (/usr/bin/catdoc $path; true) 2>/dev/null& +- wait_timeout 2>/dev/null +-elif [ $fmt = "ppt" ]; then +- (/usr/bin/catppt $path; true) 2>/dev/null& +- wait_timeout 2>/dev/null +-elif [ $fmt = "xls" ]; then +- (/usr/bin/xls2csv $path; true) 2>/dev/null& +- wait_timeout 2>/dev/null +-elif [ $fmt = "odt" -o $fmt = "ods" -o $fmt = "odp" ]; then +- xmlunzip "content.xml" +-elif [ $fmt = "docx" ]; then +- xmlunzip "document.xml" +-elif [ $fmt = "xlsx" ]; then +- xmlunzip "sharedStrings.xml" +-elif [ $fmt = "pptx" ]; then +- xmlunzip "slide*.xml" +-else +- echo "Buggy decoder script: $fmt not handled" >&2 +- exit 1 +-fi +-exit 0 diff --git a/patches/CVE-2025-59032.patch b/patches/CVE-2025-59032.patch new file mode 100644 index 0000000..7a36b02 --- /dev/null +++ b/patches/CVE-2025-59032.patch @@ -0,0 +1,30 @@ +From efb68fac3a9d2d04d38c4ab14dd570cf0c23923c Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 22 Dec 2025 22:25:04 +0200 +Subject: [PATCH] managesieve-login: Fix crash when command didn't finish on + the first call + +--- + src/managesieve-login/client.c | 9 +++++---- + 1 file changed, 5 insertions(+), 4 deletions(-) + +Index: dovecot/pigeonhole/src/managesieve-login/client.c +=================================================================== +--- dovecot.orig/pigeonhole/src/managesieve-login/client.c ++++ dovecot/pigeonhole/src/managesieve-login/client.c +@@ -336,10 +336,12 @@ static bool managesieve_client_input_nex + if (args[0].type != MANAGESIEVE_ARG_EOL) + ret = -1; + } +- } +- if (ret > 0) { ++ if (ret > 0) ++ ret = msieve_client->cmd->func(msieve_client, args); ++ } else { ++ /* Continue unfinished command */ + i_assert(msieve_client->cmd != NULL); +- ret = msieve_client->cmd->func(msieve_client, args); ++ ret = msieve_client->cmd->func(msieve_client, NULL); + } + + if (ret != 0) diff --git a/patches/CVE-2026-24031-27860-1.patch b/patches/CVE-2026-24031-27860-1.patch new file mode 100644 index 0000000..495da7c --- /dev/null +++ b/patches/CVE-2026-24031-27860-1.patch @@ -0,0 +1,39 @@ +From 466023fc20b05eb6a79d85b0d5efaa45a81ca48d Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 25 Feb 2026 09:33:25 +0200 +Subject: [PATCH 04/24] auth: Make struct settings_get_params params const + +--- + src/auth/passdb-sql.c | 2 +- + src/auth/userdb-sql.c | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/auth/passdb-sql.c b/src/auth/passdb-sql.c +index db1990dc47..f3682d4926 100644 +--- a/src/auth/passdb-sql.c ++++ b/src/auth/passdb-sql.c +@@ -182,7 +182,7 @@ static void sql_lookup_pass(struct passdb_sql_request *sql_request) + const struct passdb_sql_settings *set; + const char *error; + +- struct settings_get_params params = { ++ const struct settings_get_params params = { + .escape_func = passdb_sql_escape, + .escape_context = module->db, + }; +diff --git a/src/auth/userdb-sql.c b/src/auth/userdb-sql.c +index f97fe5e316..349f61c49e 100644 +--- a/src/auth/userdb-sql.c ++++ b/src/auth/userdb-sql.c +@@ -128,7 +128,7 @@ static void userdb_sql_lookup(struct auth_request *auth_request, + const struct userdb_sql_settings *set; + const char *error; + +- struct settings_get_params params = { ++ const struct settings_get_params params = { + .escape_func = userdb_sql_escape, + .escape_context = module->db, + }; +-- +2.39.5 + diff --git a/patches/CVE-2026-24031-27860-2.patch b/patches/CVE-2026-24031-27860-2.patch new file mode 100644 index 0000000..2f7c5a2 --- /dev/null +++ b/patches/CVE-2026-24031-27860-2.patch @@ -0,0 +1,113 @@ +From 531c0801f0431eadf91383dab25aa9a8ede0d22f Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 20 Feb 2026 18:37:38 +0200 +Subject: [PATCH 05/24] auth: passdb/userdb ldap - Fix escaping ldap filter, + base and bind_userdn + +Broken by c2ccdab8d09dec65753ee42366f48d53d7f47cfd +--- + src/auth/passdb-ldap.c | 23 +++++++++++++++++------ + src/auth/userdb-ldap.c | 23 +++++++++++++++++------ + 2 files changed, 34 insertions(+), 12 deletions(-) + +Index: dovecot/src/auth/passdb-ldap.c +=================================================================== +--- dovecot.orig/src/auth/passdb-ldap.c ++++ dovecot/src/auth/passdb-ldap.c +@@ -376,9 +376,12 @@ ldap_verify_plain(struct auth_request *r + return; + } + ++ const struct settings_get_params params = { ++ .escape_func = ldap_escape, ++ }; + const struct ldap_pre_settings *ldap_pre = NULL; +- if (settings_get(event, &ldap_pre_setting_parser_info, 0, +- &ldap_pre, &error) < 0 || ++ if (settings_get_params(event, &ldap_pre_setting_parser_info, ++ ¶ms, &ldap_pre, &error) < 0 || + ldap_pre_settings_post_check(ldap_pre, DB_LDAP_LOOKUP_TYPE_PASSDB, + &error) < 0) { + e_error(event, "%s", error); +@@ -414,10 +417,13 @@ static void ldap_lookup_credentials(stru + auth_request_ref(request); + ldap_request->request.ldap.auth_request = request; + ++ const struct settings_get_params params = { ++ .escape_func = ldap_escape, ++ }; + const char *error; + const struct ldap_pre_settings *ldap_pre = NULL; +- if (settings_get(event, &ldap_pre_setting_parser_info, 0, +- &ldap_pre, &error) < 0 || ++ if (settings_get_params(event, &ldap_pre_setting_parser_info, ¶ms, ++ &ldap_pre, &error) < 0 || + ldap_pre_settings_post_check(ldap_pre, DB_LDAP_LOOKUP_TYPE_PASSDB, + &error) < 0) { + e_error(event, "%s", error); +@@ -446,8 +452,13 @@ static int passdb_ldap_preinit(pool_t po + if (settings_get(event, &auth_passdb_post_setting_parser_info, + RAW_SETTINGS, &auth_post, error_r) < 0) + goto failed; +- if (settings_get(event, &ldap_pre_setting_parser_info, +- RAW_SETTINGS, &ldap_pre, error_r) < 0) ++ ++ const struct settings_get_params params = { ++ .escape_func = ldap_escape, ++ .flags = RAW_SETTINGS, ++ }; ++ if (settings_get_params(event, &ldap_pre_setting_parser_info, ++ ¶ms, &ldap_pre, error_r) < 0) + goto failed; + + module = p_new(pool, struct ldap_passdb_module, 1); +Index: dovecot/src/auth/userdb-ldap.c +=================================================================== +--- dovecot.orig/src/auth/userdb-ldap.c ++++ dovecot/src/auth/userdb-ldap.c +@@ -122,9 +122,12 @@ static void userdb_ldap_lookup(struct au + struct userdb_ldap_request *request; + const char *error; + ++ const struct settings_get_params params = { ++ .escape_func = ldap_escape, ++ }; + const struct ldap_pre_settings *ldap_pre = NULL; +- if (settings_get(event, &ldap_pre_setting_parser_info, 0, +- &ldap_pre, &error) < 0 || ++ if (settings_get_params(event, &ldap_pre_setting_parser_info, ¶ms, ++ &ldap_pre, &error) < 0 || + ldap_pre_settings_post_check(ldap_pre, DB_LDAP_LOOKUP_TYPE_USERDB, + &error) < 0) { + e_error(event, "%s", error); +@@ -258,9 +261,12 @@ userdb_ldap_iterate_init(struct auth_req + request = &ctx->request; + request->ctx = ctx; + ++ const struct settings_get_params params = { ++ .escape_func = ldap_escape, ++ }; + const struct ldap_pre_settings *ldap_pre = NULL; +- if (settings_get(event, &ldap_pre_setting_parser_info, 0, +- &ldap_pre, &error) < 0 || ++ if (settings_get_params(event, &ldap_pre_setting_parser_info, ¶ms, ++ &ldap_pre, &error) < 0 || + ldap_pre_settings_post_check(ldap_pre, DB_LDAP_LOOKUP_TYPE_ITERATE, + &error) < 0) { + e_error(event, "%s", error); +@@ -331,8 +337,13 @@ static int userdb_ldap_preinit(pool_t po + if (settings_get(event, &ldap_post_setting_parser_info, + RAW_SETTINGS, &ldap_post, error_r) < 0) + goto failed; +- if (settings_get(event, &ldap_pre_setting_parser_info, +- RAW_SETTINGS, &ldap_pre, error_r) < 0) ++ ++ const struct settings_get_params params = { ++ .escape_func = ldap_escape, ++ .flags = RAW_SETTINGS, ++ }; ++ if (settings_get_params(event, &ldap_pre_setting_parser_info, ++ ¶ms, &ldap_pre, error_r) < 0) + goto failed; + + module = p_new(pool, struct ldap_userdb_module, 1); diff --git a/patches/CVE-2026-24031-27860-3.patch b/patches/CVE-2026-24031-27860-3.patch new file mode 100644 index 0000000..2776cbe --- /dev/null +++ b/patches/CVE-2026-24031-27860-3.patch @@ -0,0 +1,32 @@ +From 926057d36ce5d3ec6833c6a6d3e3b732e0a39453 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 23 Feb 2026 13:37:09 +0200 +Subject: [PATCH 06/24] lib-settings: settings_get_params() - Fix using + provided escape_func + +This fixes auth-sql and auth-ldap to actually do escaping. +--- + src/lib-settings/settings.c | 10 ++++++++-- + 1 file changed, 8 insertions(+), 2 deletions(-) + +Index: dovecot/src/lib-settings/settings.c +=================================================================== +--- dovecot.orig/src/lib-settings/settings.c ++++ dovecot/src/lib-settings/settings.c +@@ -1540,8 +1540,14 @@ settings_var_expand_init(struct settings + ctx->var_params.tables_arr = array_front(&init_ctx.tables); + ctx->var_params.providers_arr = array_front(&init_ctx.providers); + ctx->var_params.contexts = array_front(&init_ctx.contexts); +- ctx->var_params.escape_func = init_ctx.escape_func; +- ctx->var_params.escape_context = init_ctx.escape_context; ++ if (ctx->escape_func != NULL) { ++ /* settings_get_params()'s escape_func overrides all others */ ++ ctx->var_params.escape_func = ctx->escape_func; ++ ctx->var_params.escape_context = ctx->escape_context; ++ } else { ++ ctx->var_params.escape_func = init_ctx.escape_func; ++ ctx->var_params.escape_context = init_ctx.escape_context; ++ } + ctx->var_params.event = ctx->event; + } + diff --git a/patches/CVE-2026-24031-27860-4.patch b/patches/CVE-2026-24031-27860-4.patch new file mode 100644 index 0000000..48970ef --- /dev/null +++ b/patches/CVE-2026-24031-27860-4.patch @@ -0,0 +1,64 @@ +From cb09189599e28fa8f46790aeb440a35ba8414908 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 23 Feb 2026 19:33:16 +0200 +Subject: [PATCH 07/24] auth: test-auth - Run Lua unit tests even when building + Lua as plugin + +--- + src/auth/Makefile.am | 7 +++++-- + src/auth/test-lua.c | 2 +- + src/auth/test-main.c | 2 +- + 3 files changed, 7 insertions(+), 4 deletions(-) + +--- dovecot-2.4.1+dfsg1.orig/src/auth/Makefile.am ++++ dovecot-2.4.1+dfsg1/src/auth/Makefile.am +@@ -131,6 +131,8 @@ auth_common_sources = \ + $(ldap_sources) \ + $(lua_sources) + ++test_auth_ldadd_plugins = ++ + headers = \ + auth.h \ + auth-cache.h \ +@@ -185,6 +187,7 @@ libauthdb_lua_la_LDFLAGS = -module -avoi + libauthdb_lua_la_LIBADD = $(LIBDOVECOT_LUA) + libauthdb_lua_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD + libauthdb_lua_la_SOURCES = $(lua_sources) ++test_auth_ldadd_plugins += libauthdb_lua.la + endif + + libauthdb_imap_la_LDFLAGS = -module -avoid-version -shared +@@ -226,8 +229,8 @@ test_auth_SOURCES = \ + test-mock.c \ + test-main.c + +-test_auth_LDADD = $(LIBDOVECOT) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS) +-test_auth_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(LIBDOVECOT_DEPS) ++test_auth_LDADD = $(test_auth_ldadd_plugins) $(LIBDOVECOT) $(LIBDOVECOT_LUA) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS) ++test_auth_DEPENDENCIES = $(test_auth_ldadd_plugins) $(pkglibexec_PROGRAMS) $(LIBDOVECOT_DEPS) + + test_mech_SOURCES = \ + $(auth_common_sources) \ +--- dovecot-2.4.1+dfsg1.orig/src/auth/test-lua.c ++++ dovecot-2.4.1+dfsg1/src/auth/test-lua.c +@@ -2,7 +2,7 @@ + + #include "test-auth.h" + +-#ifdef BUILTIN_LUA ++#ifdef HAVE_LUA + #include "istream.h" + #include "auth-settings.h" + #include "auth-request.h" +--- dovecot-2.4.1+dfsg1.orig/src/auth/test-main.c ++++ dovecot-2.4.1+dfsg1/src/auth/test-main.c +@@ -22,7 +22,7 @@ int main(int argc, char *argv[]) + TEST_NAMED(test_auth_request_var_expand) + TEST_NAMED(test_auth_request_fields) + TEST_NAMED(test_username_filter) +-#if defined(BUILTIN_LUA) ++#if defined(HAVE_LUA) + TEST_NAMED(test_db_lua) + #endif + { NULL, NULL } diff --git a/patches/CVE-2026-24031-27860-5.patch b/patches/CVE-2026-24031-27860-5.patch new file mode 100644 index 0000000..4c7c664 --- /dev/null +++ b/patches/CVE-2026-24031-27860-5.patch @@ -0,0 +1,224 @@ +From 46fbe8a5b82d548fcf5cb352bb2510bb7292174a Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 23 Feb 2026 19:54:40 +0200 +Subject: [PATCH 08/24] auth: Rewrite ldap_escape() with a unit test + +--- + src/auth/Makefile.am | 2 + + src/auth/db-ldap.c | 89 +++++++++++++++++++++++++++++++++++--------- + src/auth/db-ldap.h | 1 + + src/auth/test-auth.h | 1 + + src/auth/test-ldap.c | 39 +++++++++++++++++++ + src/auth/test-main.c | 3 ++ + 6 files changed, 117 insertions(+), 18 deletions(-) + create mode 100644 src/auth/test-ldap.c + +--- dovecot-2.4.1+dfsg1.orig/src/auth/Makefile.am ++++ dovecot-2.4.1+dfsg1/src/auth/Makefile.am +@@ -178,6 +178,7 @@ libauthdb_ldap_la_LDFLAGS = -module -avo + libauthdb_ldap_la_LIBADD = $(LIBDOVECOT_LDAP) $(LDAP_LIBS) + libauthdb_ldap_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD + libauthdb_ldap_la_SOURCES = $(ldap_sources) ++test_auth_ldadd_plugins += libauthdb_ldap.la + else + auth_libs += $(LIBDOVECOT_LDAP) + endif +@@ -225,6 +226,7 @@ test_auth_SOURCES = \ + test-auth-request-var-expand.c \ + test-auth-request-fields.c \ + test-username-filter.c \ ++ test-ldap.c \ + test-lua.c \ + test-mock.c \ + test-main.c +--- dovecot-2.4.1+dfsg1.orig/src/auth/db-ldap.c ++++ dovecot-2.4.1+dfsg1/src/auth/db-ldap.c +@@ -13,6 +13,7 @@ + #include "str.h" + #include "strescape.h" + #include "time-util.h" ++#include "unichar.h" + #include "env-util.h" + #include "settings.h" + #include "ssl-settings.h" +@@ -54,8 +55,6 @@ + #define DB_LDAP_REQUEST_MAX_ATTEMPT_COUNT 3 + #define DB_LDAP_ATTR_DN "~dn" + +-static const char *LDAP_ESCAPE_CHARS = "*,\\#+<>;\"()= "; +- + struct db_ldap_result { + int refcount; + LDAPMessage *msg; +@@ -1139,26 +1138,80 @@ void db_ldap_get_attribute_names(pool_t + *sensitive_r = array_front(&sensitive_attr_names); + } + +-#define IS_LDAP_ESCAPED_CHAR(c) \ +- ((((unsigned char)(c)) & 0x80) != 0 || strchr(LDAP_ESCAPE_CHARS, (c)) != NULL) +- +-const char *ldap_escape(const char *str, +- void *context ATTR_UNUSED) ++const char *ldap_escape(const char *input, void *context ATTR_UNUSED) + { +- string_t *ret = NULL; +- +- for (const char *p = str; *p != '\0'; p++) { +- if (IS_LDAP_ESCAPED_CHAR(*p)) { +- if (ret == NULL) { +- ret = t_str_new((size_t) (p - str) + 64); +- str_append_data(ret, str, (size_t) (p - str)); +- } +- str_printfa(ret, "\\%02X", (unsigned char)*p); +- } else if (ret != NULL) +- str_append_c(ret, *p); ++ /* This function escapes both LDAP filters and LDAP DNs. This works, ++ because both allow using the method of escaping any characters. ++ However, this isn't really recommended, since apparently some LDAP ++ servers and perhaps other tools don't handle unnecessary escaping ++ correctly. ++ ++ Another issue is what to do with invalid UTF-8. LDAP filter RFC ++ suggests just escaping invalid UTF-8. LDAP DN RFC doesn't at least ++ explicitly say that. It instead seems to imply that the string is ++ just expected to be valid UTF-8. Or perhaps to use #hex-string ++ encoding, which isn't compatible with LDAP filters. Just to be safe, ++ we'll replace invalid UTF-8 input with the UTF-8 replacement ++ character. ++ ++ So this function isn't perhaps the most standards compliant one, but ++ we don't really care, since this escaping is mainly intended to ++ prevent untrusted username input from breaking out of the filter or ++ DN. Valid usernames are unlikely to require escaping or to be ++ invalid UTF-8. */ ++ ++ /* Convert invalid UTF-8 characters to replacement characters. */ ++ size_t input_len = strlen(input); ++ string_t *str = t_str_new(64); ++ if (!uni_utf8_get_valid_data((const unsigned char *)input, ++ input_len, str)) { ++ /* Input was not valid UTF-8 */ ++ input = t_strdup(str_c(str)); ++ input_len = str_len(str); ++ str_truncate(str, 0); ++ } ++ ++ const char *escape_chars = ++ /* control characters */ ++ "\x01\x02\x03\x04\x05\x06\x07\x08" ++ "\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10" ++ "\x11\x12\x03\x14\x15\x16\x17\x18" ++ "\x19\x1a\x1b\x1c\x1d\x1e\x1f" ++ /* filter + DN */ ++ "\\" ++ /* filter only */ ++ "*()" ++ /* DN only (not including leading and trailing chars) */ ++ ",+<>;\"="; ++ size_t pos; ++ ++ /* check the leading character separately (required by DN) */ ++ if (input[0] == '#' || input[0] == ' ') ++ pos = 0; ++ else { ++ pos = strcspn(input, escape_chars); ++ if (pos == input_len) { ++ /* the last trailing space is escaped ++ (required by DN) */ ++ if (pos > 0 && input[pos - 1] == ' ') ++ pos--; ++ else ++ return input; ++ } + } + +- return ret == NULL ? str : str_c(ret); ++ do { ++ str_append_data(str, input, pos); ++ str_printfa(str, "\\%02x", (unsigned char)input[pos]); ++ input += pos + 1; ++ input_len -= pos + 1; ++ pos = strcspn(input, escape_chars); ++ /* the last trailing space is escaped (required by DN) */ ++ if (pos == input_len && pos > 0 && input[pos - 1] == ' ') ++ pos--; ++ } while (pos < input_len); ++ str_append_data(str, input, pos); ++ return str_c(str); + } + + static bool +--- dovecot-2.4.1+dfsg1.orig/src/auth/db-ldap.h ++++ dovecot-2.4.1+dfsg1/src/auth/db-ldap.h +@@ -169,6 +169,7 @@ void db_ldap_connect_delayed(struct ldap + + void db_ldap_enable_input(struct ldap_connection *conn, bool enable); + ++const char *ldap_dn_escape(const char *str, void *context); + const char *ldap_escape(const char *str, void *context); + const char *ldap_get_error(struct ldap_connection *conn); + +--- dovecot-2.4.1+dfsg1.orig/src/auth/test-auth.h ++++ dovecot-2.4.1+dfsg1/src/auth/test-auth.h +@@ -14,6 +14,7 @@ void test_auth_request_var_expand(void); + void test_auth_request_fields(void); + void test_db_dict_parse_cache_key(void); + void test_username_filter(void); ++void test_db_ldap(void); + void test_db_lua(void); + struct auth_passdb *passdb_mock(void); + void passdb_mock_mod_init(void); +--- /dev/null ++++ dovecot-2.4.1+dfsg1/src/auth/test-ldap.c +@@ -0,0 +1,39 @@ ++/* Copyright (c) 2026 Dovecot authors, see the included COPYING file */ ++ ++#include "test-auth.h" ++ ++#ifdef HAVE_LDAP ++#include "db-ldap.h" ++ ++static void test_ldap_escape(void) ++{ ++ struct { ++ const char *input; ++ const char *output; ++ } tests[] = { ++ { "", "" }, ++ { " ", "\\20" }, ++ { " ", "\\20\\20" }, ++ { "foo ", "foo\\20" }, ++ { ",", "\\2c" }, ++ { "s p a c e", "s p a c e" }, ++ { "# start-end#", "\\23 start-end#" }, ++ { " start-end2 ", "\\20 start-end2 \\20" }, ++ { "middle:,+\"\\<>;=", "middle:\\2c\\2b\\22\\5c\\3c\\3e\\3b\\3d" }, ++ { "valid-utf8:\xc3\xb1", "valid-utf8:\xc3\xb1" }, ++ { "Bad \xFF Characters", "Bad \xEF\xBF\xBD Characters" }, ++ }; ++ test_begin("ldap_escape()"); ++ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) { ++ test_assert_strcmp_idx(ldap_escape(tests[i].input, NULL), ++ tests[i].output, i); ++ } ++ test_end(); ++} ++ ++void test_db_ldap(void) ++{ ++ test_ldap_escape(); ++} ++ ++#endif +--- dovecot-2.4.1+dfsg1.orig/src/auth/test-main.c ++++ dovecot-2.4.1+dfsg1/src/auth/test-main.c +@@ -25,6 +25,9 @@ int main(int argc, char *argv[]) + #if defined(HAVE_LUA) + TEST_NAMED(test_db_lua) + #endif ++#if defined(HAVE_LDAP) ++ TEST_NAMED(test_db_ldap) ++#endif + { NULL, NULL } + }; + diff --git a/patches/CVE-2026-24031-27860-6.patch b/patches/CVE-2026-24031-27860-6.patch new file mode 100644 index 0000000..782b2b6 --- /dev/null +++ b/patches/CVE-2026-24031-27860-6.patch @@ -0,0 +1,36 @@ +From 568fe0b2f57fd7563d053e5ef3127d3a969b0acc Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 24 Feb 2026 12:24:37 +0200 +Subject: [PATCH 09/24] auth: passdb sql - Fix escaping for set_credentials() + +This was only used by OTP SASL mechanism after successful authentication, so +it practically couldn't be used for SQL injections. + +Broken by ef0c63b690e6ef9fbd53cb815dfab50d1667ba3a +--- + src/auth/passdb-sql.c | 9 +++++++-- + 1 file changed, 7 insertions(+), 2 deletions(-) + +diff --git a/src/auth/passdb-sql.c b/src/auth/passdb-sql.c +index f3682d4926..28291606ea 100644 +--- a/src/auth/passdb-sql.c ++++ b/src/auth/passdb-sql.c +@@ -258,8 +258,13 @@ static void sql_set_credentials(struct auth_request *request, + + request->mech_password = p_strdup(request->pool, new_credentials); + +- if (settings_get(authdb_event(request), &passdb_sql_setting_parser_info, 0, +- &set, &error) < 0) { ++ const struct settings_get_params params = { ++ .escape_func = passdb_sql_escape, ++ .escape_context = module->db, ++ }; ++ if (settings_get_params(authdb_event(request), ++ &passdb_sql_setting_parser_info, ¶ms, ++ &set, &error) < 0) { + e_error(authdb_event(request), "%s", error); + callback(FALSE, request); + return; +-- +2.39.5 + diff --git a/patches/CVE-2026-24031-27860-7.patch b/patches/CVE-2026-24031-27860-7.patch new file mode 100644 index 0000000..ed76e65 --- /dev/null +++ b/patches/CVE-2026-24031-27860-7.patch @@ -0,0 +1,37 @@ +From dfce1f1e7214b97ef73fa851655d176455b836fd Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 24 Feb 2026 12:26:46 +0200 +Subject: [PATCH 10/24] auth: userdb sql - Fix escaping for user iteration + +This is mostly a non-issue, since userdb iteration doesn't take any +untrusted input. + +Broken by ef0c63b690e6ef9fbd53cb815dfab50d1667ba3a +--- + src/auth/userdb-sql.c | 10 +++++++--- + 1 file changed, 7 insertions(+), 3 deletions(-) + +diff --git a/src/auth/userdb-sql.c b/src/auth/userdb-sql.c +index 349f61c49e..09bac48650 100644 +--- a/src/auth/userdb-sql.c ++++ b/src/auth/userdb-sql.c +@@ -180,9 +180,13 @@ userdb_sql_iterate_init(struct auth_request *auth_request, + ctx->ctx.context = context; + auth_request_ref(auth_request); + +- if (settings_get(authdb_event(auth_request), +- &userdb_sql_setting_parser_info, 0, +- &set, &error) < 0) { ++ const struct settings_get_params params = { ++ .escape_func = userdb_sql_escape, ++ .escape_context = module->db, ++ }; ++ if (settings_get_params(authdb_event(auth_request), ++ &userdb_sql_setting_parser_info, ¶ms, ++ &set, &error) < 0) { + e_error(authdb_event(auth_request), "%s", error); + ctx->ctx.failed = TRUE; + return &ctx->ctx; +-- +2.39.5 + diff --git a/patches/CVE-2026-24031-27860-8.patch b/patches/CVE-2026-24031-27860-8.patch new file mode 100644 index 0000000..00a484f --- /dev/null +++ b/patches/CVE-2026-24031-27860-8.patch @@ -0,0 +1,60 @@ +From c6b15902a0e399bd6bf035f7e8869d201e9394d2 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 25 Feb 2026 12:40:22 +0200 +Subject: [PATCH 11/24] lib-var-expand: Add "safe" filter to prevent escaping + output + +For example ldap_base = %{passdb:next_dn | safe} to avoid escaping the DN. +--- + src/lib-var-expand/expansion-filter.c | 9 +++++++++ + src/lib-var-expand/expansion-program.c | 3 ++- + src/lib-var-expand/var-expand-private.h | 1 + + 3 files changed, 12 insertions(+), 1 deletion(-) + +--- dovecot-2.4.1+dfsg1.orig/src/lib-var-expand/expansion-filter.c ++++ dovecot-2.4.1+dfsg1/src/lib-var-expand/expansion-filter.c +@@ -1130,6 +1130,14 @@ static int fn_text(const struct var_expa + return 0; + } + ++static int fn_safe(const struct var_expand_statement *stmt ATTR_UNUSED, ++ struct var_expand_state *state, ++ const char **error_r ATTR_UNUSED) ++{ ++ state->transfer_safe = TRUE; ++ return 0; ++} ++ + static const struct var_expand_filter var_expand_builtin_filters[] = { + { .name = "lookup", .filter = fn_lookup }, + { .name = "literal", .filter = fn_literal }, +@@ -1167,6 +1175,7 @@ static const struct var_expand_filter va + { .name = "text", .filter = fn_text }, + { .name = "encrypt", .filter = expansion_filter_encrypt }, + { .name = "decrypt", .filter = expansion_filter_decrypt }, ++ { .name = "safe", .filter = fn_safe }, + { .name = NULL } + }; + +--- dovecot-2.4.1+dfsg1.orig/src/lib-var-expand/expansion-program.c ++++ dovecot-2.4.1+dfsg1/src/lib-var-expand/expansion-program.c +@@ -140,7 +140,8 @@ int var_expand_program_execute(string_t + if (state.transfer_binary) + var_expand_state_set_transfer(&state, binary_to_hex(state.transfer->data, state.transfer->used)); + if (state.transfer_set) { +- if (!program->only_literal && params->escape_func != NULL) { ++ if (!program->only_literal && !state.transfer_safe && ++ params->escape_func != NULL) { + str_append(state.result, + params->escape_func(str_c(state.transfer), + params->escape_context)); +--- dovecot-2.4.1+dfsg1.orig/src/lib-var-expand/var-expand-private.h ++++ dovecot-2.4.1+dfsg1/src/lib-var-expand/var-expand-private.h +@@ -73,6 +73,7 @@ struct var_expand_state { + string_t *transfer; + bool transfer_set:1; + bool transfer_binary:1; ++ bool transfer_safe:1; + }; + + struct var_expand_statement { diff --git a/patches/CVE-2026-27851.patch b/patches/CVE-2026-27851.patch new file mode 100644 index 0000000..a053404 --- /dev/null +++ b/patches/CVE-2026-27851.patch @@ -0,0 +1,46 @@ +From d75c04e8ccbeb70d66d05938665fe145175ac1b5 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Sun, 29 Mar 2026 19:33:45 +0300 +Subject: [PATCH] lib-var-expand: Reset safe state when transfer is unset + +Otherwise unsafe content is treated safe. +--- + src/lib-var-expand/test-var-expand.c | 5 ++++- + src/lib-var-expand/var-expand.c | 1 + + 2 files changed, 5 insertions(+), 1 deletion(-) + +Index: dovecot/src/lib-var-expand/test-var-expand.c +=================================================================== +--- dovecot.orig/src/lib-var-expand/test-var-expand.c ++++ dovecot/src/lib-var-expand/test-var-expand.c +@@ -611,6 +611,7 @@ static void test_var_expand_escape(void) + { .key = "escape", .value = "'hello' \"world\"", }, + { .key = "first", .value = "bobby" }, + { .key = "nasty", .value = "\';-- SELECT * FROM bobby.tables" }, ++ { .key = "feisty", .value = "' OR '1'='1" }, + VAR_EXPAND_TABLE_END + }; + +@@ -653,6 +654,10 @@ static void test_var_expand_escape(void) + { .in = "%{literal(\"\\\"\\\\hello\\\\world\\\"\")}", .out = "'\"\\hello\\world\"'", .ret = 0 }, + /* Unsupported escape sequence */ + { .in = "%{literal('\\z')}", .out = "Invalid character escape", .ret = -1 }, ++ ++ /* safe filter */ ++ { .in = "%{feisty}", "'\\' OR \\'1\\'=\\'1'", .ret = 0 }, ++ { .in = "%{clean|safe} and %{feisty}", "hello world and '\\' OR \\'1\\'=\\'1'", .ret = 0 }, + }; + + const struct var_expand_params params = { +Index: dovecot/src/lib-var-expand/var-expand.c +=================================================================== +--- dovecot.orig/src/lib-var-expand/var-expand.c ++++ dovecot/src/lib-var-expand/var-expand.c +@@ -338,6 +338,7 @@ void var_expand_state_set_transfer(struc + void var_expand_state_unset_transfer(struct var_expand_state *state) + { + str_truncate(state->transfer, 0); ++ state->transfer_safe = FALSE; + state->transfer_set = FALSE; + } + diff --git a/patches/CVE-2026-27855-1.patch b/patches/CVE-2026-27855-1.patch new file mode 100644 index 0000000..f8da06f --- /dev/null +++ b/patches/CVE-2026-27855-1.patch @@ -0,0 +1,21 @@ +From f1bc3ea8ba747fcfe14ab56685d400e4e3cff130 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 9 Mar 2026 20:04:27 +0200 +Subject: [PATCH 21/24] auth: cache - Use translated username in + auth_cache_remove() + +--- + src/auth/auth-cache.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- dovecot-2.4.1+dfsg1.orig/src/auth/auth-cache.c ++++ dovecot-2.4.1+dfsg1/src/auth/auth-cache.c +@@ -474,7 +474,7 @@ void auth_cache_remove(struct auth_cache + { + struct auth_cache_node *node; + +- key = auth_request_expand_cache_key(request, key, request->fields.user); ++ key = auth_request_expand_cache_key(request, key, request->fields.translated_username); + node = hash_table_lookup(cache->hash, key); + if (node == NULL) + return; diff --git a/patches/CVE-2026-27855-2.patch b/patches/CVE-2026-27855-2.patch new file mode 100644 index 0000000..3d948b9 --- /dev/null +++ b/patches/CVE-2026-27855-2.patch @@ -0,0 +1,92 @@ +From f2119a25a439f599b0a30fc674300b8f354e5b67 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 11 Mar 2026 12:30:32 +0200 +Subject: [PATCH 22/24] auth: Move passdb event lifecycle handling to + auth_request_passdb_event_(begin|end) + +--- + src/auth/auth-request.c | 39 +++++++++++++++++++++++++++------------ + src/auth/auth-request.h | 3 +++ + 2 files changed, 30 insertions(+), 12 deletions(-) + +--- dovecot-2.4.1+dfsg1.orig/src/auth/auth-request.c ++++ dovecot-2.4.1+dfsg1/src/auth/auth-request.c +@@ -663,15 +663,10 @@ auth_request_cache_result_to_str(enum au + } + } + +-void auth_request_passdb_lookup_begin(struct auth_request *request) ++void auth_request_passdb_event_begin(struct auth_request *request) + { + struct event *event; + +- i_assert(request->passdb != NULL); +- i_assert(!request->userdb_lookup); +- +- request->passdb_cache_result = AUTH_REQUEST_CACHE_NONE; +- + /* use passdb-specific settings during the passdb lookup */ + request->set = request->passdb->auth_set; + +@@ -691,11 +686,35 @@ void auth_request_passdb_lookup_begin(st + event_set_min_log_level(event, request->passdb->auth_set->verbose ? + LOG_TYPE_INFO : LOG_TYPE_WARNING); + ++ array_push_back(&request->authdb_event, &event); ++} ++ ++void auth_request_passdb_event_end(struct auth_request *request) ++{ ++ struct event *event = authdb_event(request); ++ event_unref(&event); ++ array_pop_back(&request->authdb_event); ++ ++ /* restore protocol-specific settings */ ++ request->set = request->protocol_set; ++} ++ ++void auth_request_passdb_lookup_begin(struct auth_request *request) ++{ ++ struct event *event; ++ ++ i_assert(request->passdb != NULL); ++ i_assert(!request->userdb_lookup); ++ auth_request_passdb_event_begin(request); ++ ++ event = authdb_event(request); ++ ++ request->passdb_cache_result = AUTH_REQUEST_CACHE_NONE; ++ + e_debug(event_create_passthrough(event)-> + set_name("auth_passdb_request_started")-> + event(), + "Performing passdb lookup"); +- array_push_back(&request->authdb_event, &event); + } + + void auth_request_passdb_lookup_end(struct auth_request *request, +@@ -713,11 +732,7 @@ void auth_request_passdb_lookup_end(stru + request->passdb_cache_result)); + } + e_debug(e->event(), "Finished passdb lookup"); +- event_unref(&event); +- array_pop_back(&request->authdb_event); +- +- /* restore protocol-specific settings */ +- request->set = request->protocol_set; ++ auth_request_passdb_event_end(request); + } + + void auth_request_userdb_lookup_begin(struct auth_request *request) +--- dovecot-2.4.1+dfsg1.orig/src/auth/auth-request.h ++++ dovecot-2.4.1+dfsg1/src/auth/auth-request.h +@@ -427,6 +427,9 @@ void auth_request_log_finished(struct au + void auth_request_master_user_login_finish(struct auth_request *request); + void auth_request_fields_init(struct auth_request *request); + ++void auth_request_passdb_event_begin(struct auth_request *request); ++void auth_request_passdb_event_end(struct auth_request *request); ++ + void auth_request_passdb_lookup_begin(struct auth_request *request); + void auth_request_passdb_lookup_end(struct auth_request *request, + enum passdb_result result); diff --git a/patches/CVE-2026-27855-3.patch b/patches/CVE-2026-27855-3.patch new file mode 100644 index 0000000..cfaa7f6 --- /dev/null +++ b/patches/CVE-2026-27855-3.patch @@ -0,0 +1,44 @@ +From 8fd279db61be2054c8f3e7275766717318a2df91 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 9 Mar 2026 21:23:29 +0200 +Subject: [PATCH 23/24] auth: Initialize set_credentials event properly + +Fixes update_query +--- + src/auth/auth-request.c | 4 ++++ + src/auth/auth-worker-server.c | 2 ++ + 2 files changed, 6 insertions(+) + +--- dovecot-2.4.1+dfsg1.orig/src/auth/auth-request.c ++++ dovecot-2.4.1+dfsg1/src/auth/auth-request.c +@@ -1528,6 +1528,8 @@ void auth_request_set_credentials(struct + struct auth_passdb *passdb = request->passdb; + const char *cache_key, *new_credentials; + ++ auth_request_passdb_event_begin(request); ++ + cache_key = passdb_cache == NULL ? NULL : passdb->cache_key; + if (cache_key != NULL) + auth_cache_remove(passdb_cache, request, cache_key); +@@ -1544,6 +1546,8 @@ void auth_request_set_credentials(struct + /* this passdb doesn't support credentials update */ + callback(FALSE, request); + } ++ ++ auth_request_passdb_event_end(request); + } + + static void +--- dovecot-2.4.1+dfsg1.orig/src/auth/auth-worker-server.c ++++ dovecot-2.4.1+dfsg1/src/auth/auth-worker-server.c +@@ -505,8 +505,10 @@ auth_worker_handle_setcred(struct auth_w + } + } + ++ auth_request_passdb_event_begin(auth_request); + auth_request->passdb->passdb->iface. + set_credentials(auth_request, creds, set_credentials_callback); ++ auth_request_passdb_event_end(auth_request); + return TRUE; + } + diff --git a/patches/CVE-2026-27855-4.patch b/patches/CVE-2026-27855-4.patch new file mode 100644 index 0000000..32c71e1 --- /dev/null +++ b/patches/CVE-2026-27855-4.patch @@ -0,0 +1,30 @@ +From 535a14209810e7b0c8f94479daf580ac4c637904 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 11 Mar 2026 12:46:53 +0200 +Subject: [PATCH 24/24] auth: passdb-sql - Require update_query to be set when + used + +--- + src/auth/passdb-sql.c | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/src/auth/passdb-sql.c b/src/auth/passdb-sql.c +index 28291606ea..ec160e3d75 100644 +--- a/src/auth/passdb-sql.c ++++ b/src/auth/passdb-sql.c +@@ -270,6 +270,12 @@ static void sql_set_credentials(struct auth_request *request, + return; + } + ++ if (*set->update_query == '\0') { ++ e_error(authdb_event(request), "passdb_sql_update_query is empty"); ++ callback(FALSE, request); ++ return; ++ } ++ + sql_request = i_new(struct passdb_sql_request, 1); + sql_request->auth_request = request; + sql_request->callback.set_credentials = callback; +-- +2.39.5 + diff --git a/patches/CVE-2026-27856-1.patch b/patches/CVE-2026-27856-1.patch new file mode 100644 index 0000000..ec20d45 --- /dev/null +++ b/patches/CVE-2026-27856-1.patch @@ -0,0 +1,54 @@ +From 6ef282fab8af8faec75b0ff8cb87b11094642ba0 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 4 Mar 2026 08:05:13 +0200 +Subject: [PATCH 16/24] doveadm: client-connection - Use timing safe credential + check + +--- + src/doveadm/client-connection-http.c | 7 +++++-- + src/doveadm/client-connection-tcp.c | 4 +--- + 2 files changed, 6 insertions(+), 5 deletions(-) + +diff --git a/src/doveadm/client-connection-http.c b/src/doveadm/client-connection-http.c +index 81ae9f51da..f760548653 100644 +--- a/src/doveadm/client-connection-http.c ++++ b/src/doveadm/client-connection-http.c +@@ -973,7 +973,9 @@ doveadm_http_server_auth_basic(struct client_request_http *req, + value = p_strdup_printf(conn->conn.pool, + "doveadm:%s", set->doveadm_password); + base64_encode(value, strlen(value), b64_value); +- if (creds->data != NULL && strcmp(creds->data, str_c(b64_value)) == 0) ++ ++ if (creds->data != NULL && ++ str_equals_timing_almost_safe(value, creds->data)) + return TRUE; + + e_error(conn->conn.event, +@@ -1000,7 +1002,8 @@ doveadm_http_server_auth_api_key(struct client_request_http *req, + b64_value = str_new(conn->conn.pool, 32); + base64_encode(set->doveadm_api_key, + strlen(set->doveadm_api_key), b64_value); +- if (creds->data != NULL && strcmp(creds->data, str_c(b64_value)) == 0) ++ if (creds->data != NULL && ++ str_equals_timing_almost_safe(creds->data, str_c(b64_value))) + return TRUE; + + e_error(conn->conn.event, +diff --git a/src/doveadm/client-connection-tcp.c b/src/doveadm/client-connection-tcp.c +index 936bb03f01..cb16f7214b 100644 +--- a/src/doveadm/client-connection-tcp.c ++++ b/src/doveadm/client-connection-tcp.c +@@ -400,9 +400,7 @@ client_connection_tcp_authenticate(struct client_connection_tcp *conn) + return -1; + } + pass = t_strndup(data + 9, size - 9); +- if (strlen(pass) != strlen(set->doveadm_password) || +- !mem_equals_timing_safe(pass, set->doveadm_password, +- strlen(pass))) { ++ if (!str_equals_timing_almost_safe(pass, set->doveadm_password)) { + e_error(conn->conn.event, + "doveadm client authenticated with wrong password"); + return -1; +-- +2.39.5 + diff --git a/patches/CVE-2026-27856-2.patch b/patches/CVE-2026-27856-2.patch new file mode 100644 index 0000000..318e8eb --- /dev/null +++ b/patches/CVE-2026-27856-2.patch @@ -0,0 +1,54 @@ +From 6d8c50154744284304ceeab69c3951e6d6852007 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 4 Mar 2026 09:28:18 +0200 +Subject: [PATCH 17/24] doveadm: Use datastack for temporary b64 value + +There is no need to allocate it from connection pool. +--- + src/doveadm/client-connection-http.c | 14 +++++--------- + 1 file changed, 5 insertions(+), 9 deletions(-) + +diff --git a/src/doveadm/client-connection-http.c b/src/doveadm/client-connection-http.c +index f760548653..5d927d1979 100644 +--- a/src/doveadm/client-connection-http.c ++++ b/src/doveadm/client-connection-http.c +@@ -960,7 +960,7 @@ doveadm_http_server_auth_basic(struct client_request_http *req, + struct client_connection_http *conn = req->conn; + const struct doveadm_settings *set = conn->conn.set; + string_t *b64_value; +- char *value; ++ const char *value; + + if (*set->doveadm_password == '\0') { + e_error(conn->conn.event, +@@ -969,13 +969,11 @@ doveadm_http_server_auth_basic(struct client_request_http *req, + return FALSE; + } + +- b64_value = str_new(conn->conn.pool, 32); +- value = p_strdup_printf(conn->conn.pool, +- "doveadm:%s", set->doveadm_password); +- base64_encode(value, strlen(value), b64_value); ++ value = t_strdup_printf("doveadm:%s", set->doveadm_password); ++ b64_value = t_base64_encode_str(0, UINT_MAX, value); + + if (creds->data != NULL && +- str_equals_timing_almost_safe(value, creds->data)) ++ str_equals_timing_almost_safe(str_c(b64_value), creds->data)) + return TRUE; + + e_error(conn->conn.event, +@@ -999,9 +997,7 @@ doveadm_http_server_auth_api_key(struct client_request_http *req, + return FALSE; + } + +- b64_value = str_new(conn->conn.pool, 32); +- base64_encode(set->doveadm_api_key, +- strlen(set->doveadm_api_key), b64_value); ++ b64_value = t_base64_encode_str(0, UINT_MAX, set->doveadm_api_key); + if (creds->data != NULL && + str_equals_timing_almost_safe(creds->data, str_c(b64_value))) + return TRUE; +-- +2.39.5 + diff --git a/patches/CVE-2026-27856-3.patch b/patches/CVE-2026-27856-3.patch new file mode 100644 index 0000000..e0b5623 --- /dev/null +++ b/patches/CVE-2026-27856-3.patch @@ -0,0 +1,26 @@ +From cc628976210de17bd26b4d9761ea879ce78b7980 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 4 Mar 2026 14:39:43 +0200 +Subject: [PATCH 18/24] doveadm: client-connection - Get API key from + per-connection settings + +--- + src/doveadm/client-connection-http.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/doveadm/client-connection-http.c b/src/doveadm/client-connection-http.c +index 5d927d1979..dfbc3528d7 100644 +--- a/src/doveadm/client-connection-http.c ++++ b/src/doveadm/client-connection-http.c +@@ -987,7 +987,7 @@ doveadm_http_server_auth_api_key(struct client_request_http *req, + const struct http_auth_credentials *creds) + { + struct client_connection_http *conn = req->conn; +- const struct doveadm_settings *set = doveadm_settings; ++ const struct doveadm_settings *set = conn->conn.set; + string_t *b64_value; + + if (*set->doveadm_api_key == '\0') { +-- +2.39.5 + diff --git a/patches/CVE-2026-27857-1.patch b/patches/CVE-2026-27857-1.patch new file mode 100644 index 0000000..fb2ac2f --- /dev/null +++ b/patches/CVE-2026-27857-1.patch @@ -0,0 +1,27 @@ +From bc91df5f91e8b83c58f138799d39be687271106d Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 6 Mar 2026 17:06:45 +0200 +Subject: [PATCH 1/2] plugins: imap-filter-sieve: imap-filter-sieve - Adjust to + imap_parser_create() API change + +--- + src/plugins/imap-filter-sieve/cmd-filter-sieve.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c b/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c +index 6965b511..33d6804c 100644 +--- a/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c ++++ b/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c +@@ -379,7 +379,8 @@ bool cmd_filter_sieve(struct client_command_context *cmd) + asynchronously the same way as APPEND does. */ + client->input_lock = cmd; + ctx->parser = imap_parser_create(client->input, client->output, +- client->set->imap_max_line_length); ++ client->set->imap_max_line_length, ++ NULL); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(ctx->parser); + o_stream_unset_flush_callback(client->output); +-- +2.39.5 + diff --git a/patches/CVE-2026-27857-2.patch b/patches/CVE-2026-27857-2.patch new file mode 100644 index 0000000..37e7b7d --- /dev/null +++ b/patches/CVE-2026-27857-2.patch @@ -0,0 +1,253 @@ +From ad8e6542518acee4e1a803001b232c03732cdaff Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 6 Mar 2026 15:25:14 +0200 +Subject: [PATCH 12/24] lib-imap, global: Add params parameter to + imap_parser_create() + +--- + src/imap-login/imap-login-client.c | 4 ++-- + src/imap-login/imap-login-cmd-id.c | 4 +++- + src/imap/cmd-append.c | 3 ++- + src/imap/cmd-setmetadata.c | 3 ++- + src/imap/imap-client.c | 3 ++- + src/lib-imap-client/imapc-connection.c | 3 ++- + src/lib-imap-storage/imap-msgpart.c | 2 +- + src/lib-imap/imap-bodystructure.c | 4 ++-- + src/lib-imap/imap-envelope.c | 2 +- + src/lib-imap/imap-parser.c | 3 ++- + src/lib-imap/imap-parser.h | 6 +++++- + src/lib-imap/test-imap-parser.c | 10 +++++----- + src/plugins/virtual/virtual-config.c | 2 +- + 13 files changed, 30 insertions(+), 19 deletions(-) + +Index: dovecot/src/imap-login/imap-login-client.c +=================================================================== +--- dovecot.orig/src/imap-login/imap-login-client.c ++++ dovecot/src/imap-login/imap-login-client.c +@@ -382,7 +382,7 @@ static int imap_client_create(struct cli + imap_client->parser = + imap_parser_create(imap_client->common.input, + imap_client->common.output, +- IMAP_LOGIN_MAX_LINE_LENGTH); ++ IMAP_LOGIN_MAX_LINE_LENGTH, NULL); + struct settings_instance *set_instance = settings_instance_find(client->event); + if (set_instance == NULL) { + set_instance = settings_instance_new( +@@ -468,7 +468,7 @@ static void imap_client_starttls(struct + imap_client->parser = + imap_parser_create(imap_client->common.input, + imap_client->common.output, +- IMAP_LOGIN_MAX_LINE_LENGTH); ++ IMAP_LOGIN_MAX_LINE_LENGTH, NULL); + + /* CRLF is lost from buffer when streams are reopened. */ + imap_client->skip_line = FALSE; +Index: dovecot/src/imap-login/imap-login-cmd-id.c +=================================================================== +--- dovecot.orig/src/imap-login/imap-login-cmd-id.c ++++ dovecot/src/imap-login/imap-login-cmd-id.c +@@ -346,7 +346,8 @@ int cmd_id(struct imap_client *client) + id->params->pool = param_pool; + id->parser = imap_parser_create(client->common.input, + client->common.output, +- IMAP_LOGIN_MAX_LINE_LENGTH); ++ IMAP_LOGIN_MAX_LINE_LENGTH, ++ NULL); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(id->parser); + parser_flags = IMAP_PARSE_FLAG_STOP_AT_LIST; +Index: dovecot/src/imap/cmd-append.c +=================================================================== +--- dovecot.orig/src/imap/cmd-append.c ++++ dovecot/src/imap/cmd-append.c +@@ -1063,7 +1063,8 @@ static bool cmd_append_full(struct clien + o_stream_unset_flush_callback(client->output); + + ctx->save_parser = imap_parser_create(client->input, client->output, +- client->set->imap_max_line_length); ++ client->set->imap_max_line_length, ++ NULL); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(ctx->save_parser); + +Index: dovecot/src/imap/cmd-setmetadata.c +=================================================================== +--- dovecot.orig/src/imap/cmd-setmetadata.c ++++ dovecot/src/imap/cmd-setmetadata.c +@@ -289,7 +289,8 @@ cmd_setmetadata_start(struct imap_setmet + asynchronously the same way as APPEND does. */ + client->input_lock = cmd; + ctx->parser = imap_parser_create(client->input, client->output, +- client->set->imap_max_line_length); ++ client->set->imap_max_line_length, ++ NULL); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(ctx->parser); + o_stream_unset_flush_callback(client->output); +Index: dovecot/src/imap/imap-client.c +=================================================================== +--- dovecot.orig/src/imap/imap-client.c ++++ dovecot/src/imap/imap-client.c +@@ -975,7 +975,8 @@ client_command_new(struct client *client + } else { + cmd->parser = + imap_parser_create(client->input, client->output, +- client->set->imap_max_line_length); ++ client->set->imap_max_line_length, ++ NULL); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(cmd->parser); + } +Index: dovecot/src/lib-imap-client/imapc-connection.c +=================================================================== +--- dovecot.orig/src/lib-imap-client/imapc-connection.c ++++ dovecot/src/lib-imap-client/imapc-connection.c +@@ -1876,7 +1876,8 @@ static void imapc_connection_connect_nex + o_stream_set_flush_callback(conn->output, imapc_connection_connected, + conn); + conn->parser = imap_parser_create(conn->input, NULL, +- conn->client->set->imapc_max_line_length); ++ conn->client->set->imapc_max_line_length, ++ NULL); + conn->to = timeout_add(conn->client->set->imapc_connection_timeout_interval_msecs, + imapc_connection_timeout, conn); + conn->to_output = timeout_add(conn->client->set->imapc_max_idle_time_secs*1000, +Index: dovecot/src/lib-imap-storage/imap-msgpart.c +=================================================================== +--- dovecot.orig/src/lib-imap-storage/imap-msgpart.c ++++ dovecot/src/lib-imap-storage/imap-msgpart.c +@@ -149,7 +149,7 @@ imap_msgpart_get_header_fields(pool_t po + int result = 0; + + input = i_stream_create_from_data(header_list, strlen(header_list)); +- parser = imap_parser_create(input, NULL, SIZE_MAX); ++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + + if (imap_parser_finish_line(parser, 0, 0, &args) > 0 && + imap_arg_get_list_full(args, &hdr_list, &list_count) && +Index: dovecot/src/lib-imap/imap-bodystructure.c +=================================================================== +--- dovecot.orig/src/lib-imap/imap-bodystructure.c ++++ dovecot/src/lib-imap/imap-bodystructure.c +@@ -722,7 +722,7 @@ int imap_bodystructure_parse_full(const + input = i_stream_create_from_data(bodystructure, strlen(bodystructure)); + (void)i_stream_read(input); + +- parser = imap_parser_create(input, NULL, SIZE_MAX); ++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + ret = imap_parser_finish_line(parser, 0, + IMAP_PARSE_FLAG_LITERAL_TYPE, &args); + if (ret < 0) { +@@ -972,7 +972,7 @@ int imap_body_parse_from_bodystructure(c + input = i_stream_create_from_data(bodystructure, strlen(bodystructure)); + (void)i_stream_read(input); + +- parser = imap_parser_create(input, NULL, SIZE_MAX); ++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + ret = imap_parser_finish_line(parser, 0, IMAP_PARSE_FLAG_NO_UNESCAPE | + IMAP_PARSE_FLAG_LITERAL_TYPE, &args); + if (ret < 0) { +Index: dovecot/src/lib-imap/imap-envelope.c +=================================================================== +--- dovecot.orig/src/lib-imap/imap-envelope.c ++++ dovecot/src/lib-imap/imap-envelope.c +@@ -222,7 +222,7 @@ bool imap_envelope_parse(const char *env + input = i_stream_create_from_data(envelope, strlen(envelope)); + (void)i_stream_read(input); + +- parser = imap_parser_create(input, NULL, SIZE_MAX); ++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + ret = imap_parser_finish_line(parser, 0, + IMAP_PARSE_FLAG_LITERAL_TYPE, &args); + if (ret < 0) { +Index: dovecot/src/lib-imap/imap-parser.c +=================================================================== +--- dovecot.orig/src/lib-imap/imap-parser.c ++++ dovecot/src/lib-imap/imap-parser.c +@@ -69,7 +69,8 @@ struct imap_parser { + + struct imap_parser * + imap_parser_create(struct istream *input, struct ostream *output, +- size_t max_line_size) ++ size_t max_line_size, ++ const struct imap_parser_params *params ATTR_UNUSED) + { + struct imap_parser *parser; + +Index: dovecot/src/lib-imap/imap-parser.h +=================================================================== +--- dovecot.orig/src/lib-imap/imap-parser.h ++++ dovecot/src/lib-imap/imap-parser.h +@@ -38,6 +38,9 @@ enum imap_parser_error { + IMAP_PARSE_ERROR_LITERAL_TOO_BIG + }; + ++struct imap_parser_params { ++}; ++ + struct imap_parser; + + /* Create new IMAP argument parser. output is used for sending command +@@ -53,7 +56,8 @@ struct imap_parser; + 2 * max_line_size. */ + struct imap_parser * + imap_parser_create(struct istream *input, struct ostream *output, +- size_t max_line_size) ATTR_NULL(2); ++ size_t max_line_size, ++ const struct imap_parser_params *params); + void imap_parser_ref(struct imap_parser *parser); + void imap_parser_unref(struct imap_parser **parser); + +Index: dovecot/src/lib-imap/test-imap-parser.c +=================================================================== +--- dovecot.orig/src/lib-imap/test-imap-parser.c ++++ dovecot/src/lib-imap/test-imap-parser.c +@@ -16,7 +16,7 @@ static void test_imap_parser_crlf(void) + + test_begin("imap parser crlf handling"); + input = test_istream_create(test_input); +- parser = imap_parser_create(input, NULL, 1024); ++ parser = imap_parser_create(input, NULL, 1024, NULL); + + /* must return -2 until LF is read */ + for (i = 0; test_input[i] != '\n'; i++) { +@@ -60,7 +60,7 @@ static void test_imap_parser_partial_lis + + test_begin("imap parser partial list"); + input = test_istream_create(test_input); +- parser = imap_parser_create(input, NULL, 1024); ++ parser = imap_parser_create(input, NULL, 1024, NULL); + + (void)i_stream_read(input); + test_assert(imap_parser_read_args(parser, 0, +@@ -128,7 +128,7 @@ static void test_imap_parser_read_tag_cm + if (tests[i].type != COMMAND) { + input = test_istream_create(tests[i].input); + test_assert(i_stream_read(input) > 0); +- parser = imap_parser_create(input, NULL, 1024); ++ parser = imap_parser_create(input, NULL, 1024, NULL); + ret = imap_parser_read_tag(parser, &atom); + test_assert_idx(ret == tests[i].ret, i); + test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i); +@@ -139,7 +139,7 @@ static void test_imap_parser_read_tag_cm + if (tests[i].type != TAG) { + input = test_istream_create(tests[i].input); + test_assert(i_stream_read(input) > 0); +- parser = imap_parser_create(input, NULL, 1024); ++ parser = imap_parser_create(input, NULL, 1024, NULL); + ret = imap_parser_read_command_name(parser, &atom); + test_assert_idx(ret == tests[i].ret, i); + test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i); +Index: dovecot/src/plugins/virtual/virtual-config.c +=================================================================== +--- dovecot.orig/src/plugins/virtual/virtual-config.c ++++ dovecot/src/plugins/virtual/virtual-config.c +@@ -54,7 +54,7 @@ virtual_search_args_parse(const string_t + input = i_stream_create_from_data(str_data(rule), str_len(rule)); + (void)i_stream_read(input); + +- imap_parser = imap_parser_create(input, NULL, SIZE_MAX); ++ imap_parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + ret = imap_parser_finish_line(imap_parser, 0, 0, &args); + if (ret < 0) { + sargs = NULL; diff --git a/patches/CVE-2026-27857-3.patch b/patches/CVE-2026-27857-3.patch new file mode 100644 index 0000000..2a96640 --- /dev/null +++ b/patches/CVE-2026-27857-3.patch @@ -0,0 +1,154 @@ +From bb845a61f81be978f641b79058d4bae06f27ee39 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 6 Mar 2026 15:32:29 +0200 +Subject: [PATCH 13/24] lib-imap: Add imap_parser_params.list_count_limit + +--- + src/lib-imap/imap-parser.c | 15 ++++++++++- + src/lib-imap/imap-parser.h | 6 +++++ + src/lib-imap/test-imap-parser.c | 46 +++++++++++++++++++++++++++++++++ + 3 files changed, 66 insertions(+), 1 deletion(-) + +--- dovecot-2.4.1+dfsg1.orig/src/lib-imap/imap-parser.c ++++ dovecot-2.4.1+dfsg1/src/lib-imap/imap-parser.c +@@ -39,6 +39,7 @@ struct imap_parser { + struct istream *input; + struct ostream *output; + size_t max_line_size; ++ unsigned int list_count_limit; + enum imap_parser_flags flags; + + /* reset by imap_parser_reset(): */ +@@ -46,6 +47,7 @@ struct imap_parser { + ARRAY_TYPE(imap_arg_list) root_list; + ARRAY_TYPE(imap_arg_list) *cur_list; + struct imap_arg *list_arg; ++ unsigned int list_count; + + enum arg_parse_type cur_type; + size_t cur_pos; /* parser position in input buffer */ +@@ -70,7 +72,7 @@ struct imap_parser { + struct imap_parser * + imap_parser_create(struct istream *input, struct ostream *output, + size_t max_line_size, +- const struct imap_parser_params *params ATTR_UNUSED) ++ const struct imap_parser_params *params) + { + struct imap_parser *parser; + +@@ -81,6 +83,10 @@ imap_parser_create(struct istream *input + parser->input = input; + parser->output = output; + parser->max_line_size = max_line_size; ++ if (params != NULL && params->list_count_limit > 0) ++ parser->list_count_limit = params->list_count_limit; ++ else ++ parser->list_count_limit = UINT_MAX; + + p_array_init(&parser->root_list, parser->pool, LIST_INIT_COUNT); + parser->cur_list = &parser->root_list; +@@ -122,6 +128,7 @@ void imap_parser_reset(struct imap_parse + p_array_init(&parser->root_list, parser->pool, LIST_INIT_COUNT); + parser->cur_list = &parser->root_list; + parser->list_arg = NULL; ++ parser->list_count = 0; + + parser->cur_type = ARG_PARSE_NONE; + parser->cur_pos = 0; +@@ -210,6 +217,12 @@ static bool imap_parser_close_list(struc + parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX; + return FALSE; + } ++ if (parser->list_count >= parser->list_count_limit) { ++ parser->error_msg = "Too many '('"; ++ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX; ++ return FALSE; ++ } ++ parser->list_count++; + + arg = imap_arg_create(parser); + arg->type = IMAP_ARG_EOL; +--- dovecot-2.4.1+dfsg1.orig/src/lib-imap/imap-parser.h ++++ dovecot-2.4.1+dfsg1/src/lib-imap/imap-parser.h +@@ -39,6 +39,12 @@ enum imap_parser_error { + }; + + struct imap_parser_params { ++ /* How many open lists ('(' chars) to allow before faililng the parsing. ++ 0 means unlimited. This is mainly used to prevent excessive memory ++ usage in imap-login process. In imap process there are many other ++ ways to increase memory usage, so we let the max_line_size be the ++ only limit. */ ++ unsigned int list_count_limit; + }; + + struct imap_parser; +--- dovecot-2.4.1+dfsg1.orig/src/lib-imap/test-imap-parser.c ++++ dovecot-2.4.1+dfsg1/src/lib-imap/test-imap-parser.c +@@ -2,6 +2,7 @@ + + #include "lib.h" + #include "istream.h" ++#include "istream-chain.h" + #include "imap-parser.h" + #include "test-common.h" + +@@ -79,6 +80,50 @@ static void test_imap_parser_partial_lis + test_end(); + } + ++static void test_imap_parser_list_limit(void) ++{ ++ struct { ++ const char *input; ++ int ret; ++ } tests[] = { ++ { "(())\r\n", 1 }, ++ { "((()))\r\n", -1 }, ++ }; ++ struct istream_chain *chain; ++ struct istream *chain_input; ++ struct imap_parser *parser; ++ const struct imap_arg *args; ++ ++ test_begin("imap parser list limit"); ++ struct imap_parser_params params = { ++ .list_count_limit = 2, ++ }; ++ ++ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) { ++ chain_input = i_stream_create_chain(&chain, SIZE_MAX); ++ parser = imap_parser_create(chain_input, NULL, 1024, ¶ms); ++ ++ for (unsigned int j = 0; j < 2; j++) { ++ struct istream *input = ++ test_istream_create(tests[i].input); ++ i_stream_chain_append(chain, input); ++ i_stream_unref(&input); ++ ++ (void)i_stream_read(chain_input); ++ ++ test_assert_cmp(imap_parser_read_args(parser, 0, 0, &args), ==, tests[i].ret); ++ /* skip over CRLF */ ++ i_stream_skip(chain_input, i_stream_get_data_size(chain_input)); ++ ++ /* make sure parser reset works */ ++ imap_parser_reset(parser); ++ } ++ imap_parser_unref(&parser); ++ i_stream_destroy(&chain_input); ++ } ++ test_end(); ++} ++ + static void test_imap_parser_read_tag_cmd(void) + { + enum read_type { +@@ -155,6 +200,7 @@ int main(void) + static void (*const test_functions[])(void) = { + test_imap_parser_crlf, + test_imap_parser_partial_list, ++ test_imap_parser_list_limit, + test_imap_parser_read_tag_cmd, + NULL + }; diff --git a/patches/CVE-2026-27857-4.patch b/patches/CVE-2026-27857-4.patch new file mode 100644 index 0000000..1734f02 --- /dev/null +++ b/patches/CVE-2026-27857-4.patch @@ -0,0 +1,83 @@ +From ab084588f615b7f2bb6e18c8ee9176511b647aca Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 6 Mar 2026 15:35:12 +0200 +Subject: [PATCH 14/24] imap-login: Limit the number of open IMAP parser lists + +This prevents attackers from using a large number of '(' in a command to +grow memory usage excessively. +--- + src/imap-login/imap-login-client.c | 10 ++++++++-- + src/imap-login/imap-login-client.h | 4 ++++ + src/imap-login/imap-login-cmd-id.c | 6 +++++- + 3 files changed, 17 insertions(+), 3 deletions(-) + +Index: dovecot/src/imap-login/imap-login-client.c +=================================================================== +--- dovecot.orig/src/imap-login/imap-login-client.c ++++ dovecot/src/imap-login/imap-login-client.c +@@ -379,10 +379,13 @@ static int imap_client_create(struct cli + return -1; + } + ++ struct imap_parser_params params = { ++ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, ++ }; + imap_client->parser = + imap_parser_create(imap_client->common.input, + imap_client->common.output, +- IMAP_LOGIN_MAX_LINE_LENGTH, NULL); ++ IMAP_LOGIN_MAX_LINE_LENGTH, ¶ms); + struct settings_instance *set_instance = settings_instance_find(client->event); + if (set_instance == NULL) { + set_instance = settings_instance_new( +@@ -464,11 +467,14 @@ static void imap_client_starttls(struct + struct imap_client *imap_client = + container_of(client, struct imap_client, common); + ++ struct imap_parser_params params = { ++ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, ++ }; + imap_parser_unref(&imap_client->parser); + imap_client->parser = + imap_parser_create(imap_client->common.input, + imap_client->common.output, +- IMAP_LOGIN_MAX_LINE_LENGTH, NULL); ++ IMAP_LOGIN_MAX_LINE_LENGTH, ¶ms); + + /* CRLF is lost from buffer when streams are reopened. */ + imap_client->skip_line = FALSE; +Index: dovecot/src/imap-login/imap-login-client.h +=================================================================== +--- dovecot.orig/src/imap-login/imap-login-client.h ++++ dovecot/src/imap-login/imap-login-client.h +@@ -11,6 +11,10 @@ + /* maximum length for IMAP command line. */ + #define IMAP_LOGIN_MAX_LINE_LENGTH 8192 + ++/* Maximum number of '(' allowed in an IMAP command. Pre-login only uses ++ lists in the ID command. */ ++#define IMAP_LOGIN_LIST_COUNT_LIMIT 1 ++ + enum imap_client_id_state { + IMAP_CLIENT_ID_STATE_LIST = 0, + IMAP_CLIENT_ID_STATE_KEY, +Index: dovecot/src/imap-login/imap-login-cmd-id.c +=================================================================== +--- dovecot.orig/src/imap-login/imap-login-cmd-id.c ++++ dovecot/src/imap-login/imap-login-cmd-id.c +@@ -344,10 +344,14 @@ int cmd_id(struct imap_client *client) + client->cmd_id = id = i_new(struct imap_client_cmd_id, 1); + id->params = p_new(param_pool, struct imap_id_params, 1); + id->params->pool = param_pool; ++ ++ struct imap_parser_params params = { ++ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, ++ }; + id->parser = imap_parser_create(client->common.input, + client->common.output, + IMAP_LOGIN_MAX_LINE_LENGTH, +- NULL); ++ ¶ms); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(id->parser); + parser_flags = IMAP_PARSE_FLAG_STOP_AT_LIST; diff --git a/patches/CVE-2026-27857-5.patch b/patches/CVE-2026-27857-5.patch new file mode 100644 index 0000000..4496bf6 --- /dev/null +++ b/patches/CVE-2026-27857-5.patch @@ -0,0 +1,53 @@ +From e0b953af76d754c2984c2867b71fccfef5231bf5 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 2 Mar 2026 13:50:24 +0200 +Subject: [PATCH 15/24] global: Use const for struct imap_parser_params params + +--- + src/imap-login/imap-login-client.c | 4 ++-- + src/imap-login/imap-login-cmd-id.c | 2 +- + src/lib-imap/test-imap-parser.c | 2 +- + 3 files changed, 4 insertions(+), 4 deletions(-) + +--- dovecot-2.4.1+dfsg1.orig/src/imap-login/imap-login-client.c ++++ dovecot-2.4.1+dfsg1/src/imap-login/imap-login-client.c +@@ -379,7 +379,7 @@ static int imap_client_create(struct cli + return -1; + } + +- struct imap_parser_params params = { ++ const struct imap_parser_params params = { + .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, + }; + imap_client->parser = +@@ -467,7 +467,7 @@ static void imap_client_starttls(struct + struct imap_client *imap_client = + container_of(client, struct imap_client, common); + +- struct imap_parser_params params = { ++ const struct imap_parser_params params = { + .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, + }; + imap_parser_unref(&imap_client->parser); +--- dovecot-2.4.1+dfsg1.orig/src/imap-login/imap-login-cmd-id.c ++++ dovecot-2.4.1+dfsg1/src/imap-login/imap-login-cmd-id.c +@@ -345,7 +345,7 @@ int cmd_id(struct imap_client *client) + id->params = p_new(param_pool, struct imap_id_params, 1); + id->params->pool = param_pool; + +- struct imap_parser_params params = { ++ const struct imap_parser_params params = { + .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, + }; + id->parser = imap_parser_create(client->common.input, +--- dovecot-2.4.1+dfsg1.orig/src/lib-imap/test-imap-parser.c ++++ dovecot-2.4.1+dfsg1/src/lib-imap/test-imap-parser.c +@@ -95,7 +95,7 @@ static void test_imap_parser_list_limit( + const struct imap_arg *args; + + test_begin("imap parser list limit"); +- struct imap_parser_params params = { ++ const struct imap_parser_params params = { + .list_count_limit = 2, + }; + diff --git a/patches/CVE-2026-27858.patch b/patches/CVE-2026-27858.patch new file mode 100644 index 0000000..0df753d --- /dev/null +++ b/patches/CVE-2026-27858.patch @@ -0,0 +1,31 @@ +From fcb339d39bfb19abf1bb88711112f49c1c589b1a Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 2 Mar 2026 14:40:57 +0200 +Subject: [PATCH 2/2] managesieve-login: Verify AUTHENTICATE initial response + size isn't too large + +This prevents DoSing the managesieve-login by sending an excessively large +initial response size, which causes a huge memory allocation. +--- + src/managesieve-login/client-authenticate.c | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/pigeonhole/src/managesieve-login/client-authenticate.c b/pigeonhole/src/managesieve-login/client-authenticate.c +index 822dae6a..5aeccd35 100644 +--- a/pigeonhole/src/managesieve-login/client-authenticate.c ++++ b/pigeonhole/src/managesieve-login/client-authenticate.c +@@ -196,6 +196,11 @@ managesieve_client_auth_read_response(struct managesieve_client *msieve_client, + if (i_stream_get_size(msieve_client->auth_response_input, + FALSE, &resp_size) <= 0) + resp_size = 0; ++ else if (resp_size > LOGIN_MAX_AUTH_BUF_SIZE) { ++ client_destroy(client, ++ "Authentication response too large"); ++ return -1; ++ } + + if (client->auth_response == NULL) { + client->auth_response = +-- +2.39.5 + diff --git a/patches/CVE-2026-27859.patch b/patches/CVE-2026-27859.patch new file mode 100644 index 0000000..1fa2ab3 --- /dev/null +++ b/patches/CVE-2026-27859.patch @@ -0,0 +1,100 @@ +From 4041a0cb7bbaa6faf38670ab92afc1039dd0d0d5 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 24 Feb 2026 13:11:14 +0200 +Subject: [PATCH 03/24] lib-mail: Limit the number of RFC2231 parameters that + can be parsed + +This avoids excessive CPU usage especially in result_append(). +--- + src/lib-mail/rfc2231-parser.c | 4 +++- + src/lib-mail/rfc822-parser.h | 5 +++++ + src/lib-mail/test-rfc2231-parser.c | 30 ++++++++++++++++++++++++++++++ + 3 files changed, 38 insertions(+), 1 deletion(-) + +--- dovecot-2.4.1+dfsg1.orig/src/lib-mail/rfc2231-parser.c ++++ dovecot-2.4.1+dfsg1/src/lib-mail/rfc2231-parser.c +@@ -203,7 +203,7 @@ int rfc2231_parse(struct rfc822_parser_c + struct rfc2231_parameter rfc2231_param; + const char *key, *p, *p2; + string_t *str; +- unsigned int i, j, count, next, next_idx; ++ unsigned int i, j, count, next, next_idx, params_count = 0; + bool ok, broken = FALSE; + const char *prev_replacement_str; + int ret; +@@ -221,6 +221,8 @@ int rfc2231_parse(struct rfc822_parser_c + t_array_init(&rfc2231_params_arr, 8); + str = t_str_new(64); + while ((ret = rfc822_parse_content_param(ctx, &key, str)) != 0) { ++ if (++params_count > RFC2231_MAX_PARAMS) ++ break; + if (ret < 0) { + /* try to continue anyway.. */ + broken = TRUE; +--- dovecot-2.4.1+dfsg1.orig/src/lib-mail/rfc822-parser.h ++++ dovecot-2.4.1+dfsg1/src/lib-mail/rfc822-parser.h +@@ -3,6 +3,11 @@ + + #include "unichar.h" + ++/* Maximum number of parameters to parse. After this the rest of the parameters ++ are skipped. This is to avoid excessive CPU usage that can be caused by ++ merging of these parameters. */ ++#define RFC2231_MAX_PARAMS 128 ++ + /* This can be used as a common NUL replacement character */ + #define RFC822_NUL_REPLACEMENT_STR UNICODE_REPLACEMENT_CHAR_UTF8 + +--- dovecot-2.4.1+dfsg1.orig/src/lib-mail/test-rfc2231-parser.c ++++ dovecot-2.4.1+dfsg1/src/lib-mail/test-rfc2231-parser.c +@@ -1,6 +1,7 @@ + /* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */ + + #include "lib.h" ++#include "str.h" + #include "rfc822-parser.h" + #include "rfc2231-parser.h" + #include "test-common.h" +@@ -249,6 +250,34 @@ static void test_rfc2231_parser_multi_en + run_test("rfc2231 parser invalid encoding", input, sizeof(input)-1, output); + } + ++static void test_rfc2231_parser_limits(void) ++{ ++ string_t *input = t_str_new(1024); ++ ++ test_begin("rfc2231 parser limits"); ++ str_append(input, "; "); ++ for (unsigned int i = 0; i < 1100; i++) ++ str_printfa(input, "a%u=b%u; ", i, i); ++ struct rfc822_parser_context parser; ++ const char *const *result; ++ rfc822_parser_init(&parser, str_data(input), str_len(input), NULL); ++ test_assert(rfc2231_parse(&parser, &result) == 0); ++ ++ unsigned int count = str_array_length(result); ++ test_assert(count == RFC2231_MAX_PARAMS * 2); ++ for (unsigned int i = 0; i < count; i += 2) { ++ str_truncate(input, 0); ++ str_printfa(input, "a%u", i / 2); ++ test_assert_strcmp_idx(result[i], str_c(input), i); ++ ++ str_truncate(input, 0); ++ str_printfa(input, "b%u", i / 2); ++ test_assert_strcmp_idx(result[i + 1], str_c(input), i); ++ } ++ rfc822_parser_deinit(&parser); ++ test_end(); ++} ++ + int main(void) + { + static void (*const test_functions[])(void) = { +@@ -264,6 +293,7 @@ int main(void) + test_rfc2231_parser_encodings, + test_rfc2231_parser_utf8_encoding, + test_rfc2231_parser_multi_encodings, ++ test_rfc2231_parser_limits, + NULL + }; + return test_run(test_functions); diff --git a/patches/CVE-2026-33603.patch b/patches/CVE-2026-33603.patch new file mode 100644 index 0000000..fb668a6 --- /dev/null +++ b/patches/CVE-2026-33603.patch @@ -0,0 +1,36 @@ +From c1c53885bda550632b944dd305013cd010e0e058 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 8 Apr 2026 11:33:11 +0300 +Subject: [PATCH] login-common: Only accept base64 in sasl + +--- + src/login-common/client-common-auth.c | 9 +++++++++ + 1 file changed, 9 insertions(+) + +Index: dovecot/src/login-common/client-common-auth.c +=================================================================== +--- dovecot.orig/src/login-common/client-common-auth.c ++++ dovecot/src/login-common/client-common-auth.c +@@ -3,6 +3,7 @@ + #include "hostpid.h" + #include "login-common.h" + #include "array.h" ++#include "base64.h" + #include "iostream.h" + #include "istream.h" + #include "ostream.h" +@@ -865,6 +866,14 @@ void client_auth_respond(struct client * + return; + } + ++ /* Only accept base64 */ ++ for (size_t i = 0; response[i] != '\0'; i++) { ++ if (!base64_is_valid_char(response[i]) && response[i] != '=') { ++ client_auth_fail(client, "Invalid base64 in response"); ++ return; ++ } ++ } ++ + client->auth_client_continue_pending = FALSE; + client_set_auth_waiting(client); + sasl_server_auth_continue(client, response); diff --git a/patches/CVE-2026-40016.patch b/patches/CVE-2026-40016.patch new file mode 100644 index 0000000..b14198c --- /dev/null +++ b/patches/CVE-2026-40016.patch @@ -0,0 +1,255 @@ +From 5b0ed9d1034c023d3daf218b6b8656f0cdd383dc Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sun, 19 Apr 2026 23:10:29 +0000 +Subject: [PATCH] lib-sieve: Enforce CPU time limit within :contains and + :matches matcher loops + +The naive O(N*M) substring search in mcht-contains.c and the naive find loop +in mcht-matches.c can run for hours on a large value (e.g. a message body), +completely bypassing sieve_max_cpu_time because that limit was only checked +between bytecode operations. + +Expose the active CPU limit via sieve_runtime_cpu_limit_exceeded() and poll +it every 4096 inner iterations. When the limit is hit the match returns +SIEVE_EXEC_RESOURCE_LIMIT, matching the existing behavior at the bytecode +boundary. This is a minimal safety net ahead of switching the matchers to +algorithms that do not require it. +--- + src/lib-sieve/mcht-contains.c | 21 ++++++++++++ + src/lib-sieve/mcht-matches.c | 57 +++++++++++++++++++++++++++---- + src/lib-sieve/sieve-interpreter.c | 19 +++++++++++ + src/lib-sieve/sieve-interpreter.h | 12 +++++++ + 4 files changed, 103 insertions(+), 6 deletions(-) + +Index: dovecot/pigeonhole/src/lib-sieve/mcht-contains.c +=================================================================== +--- dovecot.orig/pigeonhole/src/lib-sieve/mcht-contains.c ++++ dovecot/pigeonhole/src/lib-sieve/mcht-contains.c +@@ -8,6 +8,7 @@ + + #include "sieve-match-types.h" + #include "sieve-comparators.h" ++#include "sieve-interpreter.h" + #include "sieve-match.h" + + #include +@@ -38,7 +39,14 @@ const struct sieve_match_type_def contai + + /* FIXME: Naive substring match implementation. Should switch to more + * efficient algorithm if large values need to be searched (e.g. message body). ++ * ++ * The inner loop polls the interpreter CPU time limit periodically so that a ++ * single O(N*M) match on a large value cannot run for many times the ++ * configured sieve_max_cpu_time (which is otherwise only checked between ++ * bytecode operations). + */ ++#define SIEVE_CONTAINS_CPU_CHECK_INTERVAL 4096 ++ + static int mcht_contains_match_key + (struct sieve_match_context *mctx, const char *val, size_t val_size, + const char *key, size_t key_size) +@@ -48,6 +56,7 @@ static int mcht_contains_match_key + const char *kend = (const char *) key + key_size; + const char *vp = val; + const char *kp = key; ++ unsigned int counter = 0; + + if ( val_size == 0 ) + return ( key_size == 0 ? 1 : 0 ); +@@ -58,6 +67,18 @@ static int mcht_contains_match_key + while ( (vp < vend) && (kp < kend) ) { + if ( !cmp->def->char_match(cmp, &vp, vend, &kp, kend) ) + vp++; ++ ++ if ( ++counter >= SIEVE_CONTAINS_CPU_CHECK_INTERVAL ) { ++ counter = 0; ++ if ( sieve_runtime_cpu_limit_exceeded(mctx->runenv) ) { ++ sieve_runtime_error( ++ mctx->runenv, NULL, ++ "execution exceeded CPU time limit"); ++ mctx->exec_status = ++ SIEVE_EXEC_RESOURCE_LIMIT; ++ return -1; ++ } ++ } + } + + return ( kp == kend ? 1 : 0 ); +Index: dovecot/pigeonhole/src/lib-sieve/mcht-matches.c +=================================================================== +--- dovecot.orig/pigeonhole/src/lib-sieve/mcht-matches.c ++++ dovecot/pigeonhole/src/lib-sieve/mcht-matches.c +@@ -9,6 +9,7 @@ + + #include "sieve-match-types.h" + #include "sieve-comparators.h" ++#include "sieve-interpreter.h" + #include "sieve-match.h" + + #include +@@ -46,16 +47,38 @@ const struct sieve_match_type_def matche + #endif + + /* FIXME: Naive implementation, substitute this with dovecot src/lib/str-find.c ++ * ++ * The inner loop polls the interpreter CPU time limit periodically so that a ++ * single O(N*M) search on a large value cannot run for many times the ++ * configured sieve_max_cpu_time. Returns 1 on match, 0 on exhaustion, or -1 ++ * when the CPU time limit was exceeded (mctx->exec_status is set). + */ +-static inline bool _string_find(const struct sieve_comparator *cmp, +- const char **valp, const char *vend, const char **keyp, const char *kend) ++#define SIEVE_MATCHES_CPU_CHECK_INTERVAL 4096 ++ ++static int ++_string_find(struct sieve_match_context *mctx, ++ const struct sieve_comparator *cmp, ++ const char **valp, const char *vend, ++ const char **keyp, const char *kend, ++ unsigned int *counter) + { + while ( (*valp < vend) && (*keyp < kend) ) { + if ( !cmp->def->char_match(cmp, valp, vend, keyp, kend) ) + (*valp)++; ++ if (++(*counter) >= SIEVE_MATCHES_CPU_CHECK_INTERVAL) { ++ *counter = 0; ++ if (sieve_runtime_cpu_limit_exceeded(mctx->runenv)) { ++ sieve_runtime_error( ++ mctx->runenv, NULL, ++ "execution exceeded CPU time limit"); ++ mctx->exec_status = ++ SIEVE_EXEC_RESOURCE_LIMIT; ++ return -1; ++ } ++ } + } + +- return (*keyp == kend); ++ return (*keyp == kend ? 1 : 0); + } + + static char _scan_key_section +@@ -93,6 +116,7 @@ static int mcht_matches_match_key + char wcard = '\0'; /* Current wildcard */ + char next_wcard = '\0'; /* Next widlcard */ + unsigned int key_offset = 0; ++ unsigned int counter = 0; + + if ( cmp->def == NULL || cmp->def->char_match == NULL ) + return 0; +@@ -134,6 +158,19 @@ static int mcht_matches_match_key + while (kp < kend && vp < vend ) { + const char *needle, *nend; + ++ if (++counter >= SIEVE_MATCHES_CPU_CHECK_INTERVAL) { ++ counter = 0; ++ if (sieve_runtime_cpu_limit_exceeded(mctx->runenv)) { ++ sieve_runtime_error( ++ mctx->runenv, NULL, ++ "execution exceeded CPU time limit"); ++ mctx->exec_status = ++ SIEVE_EXEC_RESOURCE_LIMIT; ++ sieve_match_values_abort(&mvalues); ++ return -1; ++ } ++ } ++ + if ( !backtrack ) { + /* Search the next '*' wildcard in the key string */ + +@@ -268,10 +305,20 @@ static int mcht_matches_match_key + + /* Match may happen at any offset (>= key offset): find substring */ + vp += key_offset; +- if ( (vp >= vend) || !_string_find(cmp, &vp, vend, &needle, nend) ) { ++ if (vp >= vend) { + debug_printf(" failed to find needle at an offset\n"); + break; + } ++ int fres = _string_find(mctx, cmp, &vp, vend, ++ &needle, nend, &counter); ++ if (fres < 0) { ++ sieve_match_values_abort(&mvalues); ++ return -1; ++ } ++ if (fres == 0) { ++ debug_printf(" failed to find needle at an offset\n"); ++ break; ++ } + + prv = vp - str_len(section); + prk = kp; +Index: dovecot/pigeonhole/src/lib-sieve/sieve-interpreter.c +=================================================================== +--- dovecot.orig/pigeonhole/src/lib-sieve/sieve-interpreter.c ++++ dovecot/pigeonhole/src/lib-sieve/sieve-interpreter.c +@@ -85,6 +85,13 @@ struct sieve_interpreter { + struct sieve_runtime_trace trace; + struct sieve_resource_usage rusage; + ++ /* CPU time limit for the current sieve_interpreter_continue() call; ++ NULL when no limit is configured or not currently executing. Exposed ++ via sieve_runtime_cpu_limit_exceeded() so long-running runtime code ++ can enforce the limit without waiting for the next bytecode ++ boundary. */ ++ struct cpu_limit *climit; ++ + /* Current operation */ + struct sieve_operation oprtn; + +@@ -362,6 +369,15 @@ sieve_interpreter_svinst(struct sieve_in + return interp->runenv.exec_env->svinst; + } + ++bool sieve_runtime_cpu_limit_exceeded(const struct sieve_runtime_env *renv) ++{ ++ struct sieve_interpreter *interp = renv->interp; ++ ++ if (interp->climit == NULL) ++ return FALSE; ++ return cpu_limit_exceeded(interp->climit); ++} ++ + /* Do not use this function for normal sieve extensions. This is intended for + * the testsuite only. + */ +@@ -939,6 +955,7 @@ int sieve_interpreter_continue(struct si + climit = cpu_limit_init(svinst->set->max_cpu_time, + CPU_LIMIT_TYPE_USER); + } ++ interp->climit = climit; + + while (ret == SIEVE_EXEC_OK && !interp->interrupted && + *address < sieve_binary_block_get_size(renv->sblock)) { +@@ -959,6 +976,8 @@ int sieve_interpreter_continue(struct si + ret = sieve_interpreter_operation_execute(interp); + } + ++ interp->climit = NULL; ++ + if (climit != NULL) { + sieve_resource_usage_init(&rusage); + rusage.cpu_time_msecs = +Index: dovecot/pigeonhole/src/lib-sieve/sieve-interpreter.h +=================================================================== +--- dovecot.orig/pigeonhole/src/lib-sieve/sieve-interpreter.h ++++ dovecot/pigeonhole/src/lib-sieve/sieve-interpreter.h +@@ -165,6 +165,18 @@ int sieve_interpreter_run(struct sieve_i + struct sieve_result *result); + + /* ++ * CPU limit ++ */ ++ ++/* Returns TRUE if the current interpreter execution has exceeded its CPU ++ time limit (sieve_max_cpu_time). Callable from within long-running runtime ++ code (e.g. matcher inner loops) so that limit enforcement is not deferred ++ until the next bytecode boundary. Returns FALSE if no limit is active or ++ no execution is currently in progress. Cheap: does not call getrusage() ++ on each invocation. */ ++bool sieve_runtime_cpu_limit_exceeded(const struct sieve_runtime_env *renv); ++ ++/* + * Error handling + */ + diff --git a/patches/CVE-2026-40020-1.patch b/patches/CVE-2026-40020-1.patch new file mode 100644 index 0000000..c9b2c2e --- /dev/null +++ b/patches/CVE-2026-40020-1.patch @@ -0,0 +1,78 @@ +From b7daa4104ff064c1fb549540cc9d96c2d9e2509c Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 22 Apr 2026 15:43:58 +0300 +Subject: [PATCH 1/3] acl: Add acl_id_is_valid() + +Returns TRUE if the ACL identifier string is at most ACL_ID_MAX_LEN +(1024) bytes long, contains no control characters and is valid UTF-8. +--- + src/plugins/acl/acl-rights.c | 16 ++++++++++++++++ + src/plugins/acl/acl-rights.h | 6 ++++++ + 2 files changed, 22 insertions(+) + +diff --git a/src/plugins/acl/acl-rights.c b/src/plugins/acl/acl-rights.c +index 654ee00aef..5729f8b52a 100644 +--- a/src/plugins/acl/acl-rights.c ++++ b/src/plugins/acl/acl-rights.c +@@ -3,11 +3,14 @@ + #include "lib.h" + #include "array.h" + #include "str.h" ++#include "unichar.h" + /* */ + #include "strescape.h" + /* */ + #include "acl-api-private.h" + ++#include ++ + /* */ + const struct acl_letter_map acl_letter_map[] = { + { 'l', MAIL_ACL_LOOKUP }, +@@ -44,6 +47,19 @@ static_assert(N_ELEMENTS(acl_letter_map) == N_ELEMENTS(all_mailbox_rights), + + /* */ + ++bool acl_id_is_valid(const char *id) ++{ ++ size_t len = strlen(id); ++ ++ if (len > ACL_ID_MAX_LEN) ++ return FALSE; ++ for (size_t i = 0; i < len; i++) { ++ if (i_iscntrl(id[i])) ++ return FALSE; ++ } ++ return uni_utf8_data_is_valid((const unsigned char *)id, len); ++} ++ + void acl_rights_write_id(string_t *dest, const struct acl_rights *right) + { + switch (right->id_type) { +diff --git a/src/plugins/acl/acl-rights.h b/src/plugins/acl/acl-rights.h +index 88ef73e3b4..32163e7c3d 100644 +--- a/src/plugins/acl/acl-rights.h ++++ b/src/plugins/acl/acl-rights.h +@@ -34,6 +34,8 @@ + #define ACL_ID_NAME_GROUP_PREFIX "group=" + #define ACL_ID_NAME_GROUP_OVERRIDE_PREFIX "group-override=" + ++#define ACL_ID_MAX_LEN 1024 ++ + struct acl_letter_map { + const char letter; + const char *name; +@@ -104,6 +106,10 @@ struct acl_rights_update { + time_t last_change; + }; + ++/* Returns TRUE if the ACL identifier string is valid: no longer than ++ ACL_ID_MAX_LEN bytes, no control characters and valid UTF-8. */ ++bool acl_id_is_valid(const char *id); ++ + /* Returns the canonical ID for the right. */ + const char *acl_rights_get_id(const struct acl_rights *right); + +-- +2.47.3 + diff --git a/patches/CVE-2026-40020-2.patch b/patches/CVE-2026-40020-2.patch new file mode 100644 index 0000000..62f437f --- /dev/null +++ b/patches/CVE-2026-40020-2.patch @@ -0,0 +1,28 @@ +From 20b48c3db5fed7ccaa8e0a4c10ca54f6dc36a63d Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 22 Apr 2026 15:44:24 +0300 +Subject: [PATCH 2/3] imap-acl: Fail if ACL identifier is invalid + +Reject invalid identifiers early in imap_acl_identifier_parse() using +acl_id_is_valid(). This prevents CR/LF injection and rejects identifiers +that are too long, contain control characters or are not valid UTF-8. +--- + src/plugins/imap-acl/imap-acl-plugin.c | 5 +++++ + 1 file changed, 5 insertions(+) + +Index: dovecot/src/plugins/imap-acl/imap-acl-plugin.c +=================================================================== +--- dovecot.orig/src/plugins/imap-acl/imap-acl-plugin.c ++++ dovecot/src/plugins/imap-acl/imap-acl-plugin.c +@@ -872,6 +872,11 @@ imap_acl_identifier_parse(struct client_ + allow_anyone = set->allow_anyone; + settings_free(set); + ++ if (!acl_id_is_valid(id)) { ++ *client_error_r = "Invalid identifier"; ++ return -1; ++ } ++ + if (str_begins_with(id, IMAP_ACL_GLOBAL_PREFIX)) { + *client_error_r = t_strdup_printf( + "Global ACLs can't be modified: %s", id); diff --git a/patches/CVE-2026-40020-3.patch b/patches/CVE-2026-40020-3.patch new file mode 100644 index 0000000..03fabcc --- /dev/null +++ b/patches/CVE-2026-40020-3.patch @@ -0,0 +1,23 @@ +From 1cf6ad1a119e5dace816e401e73ba6cc11d1472e Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 22 Apr 2026 15:45:00 +0300 +Subject: [PATCH 3/3] acl: Assert-crash if ACL identifier is invalid before + writing it + +It should have been checked earlier already. +--- + src/plugins/acl/acl-backend-vfile-update.c | 1 + + 1 file changed, 1 insertion(+) + +Index: dovecot/src/plugins/acl/acl-backend-vfile-update.c +=================================================================== +--- dovecot.orig/src/plugins/acl/acl-backend-vfile-update.c ++++ dovecot/src/plugins/acl/acl-backend-vfile-update.c +@@ -119,6 +119,7 @@ vfile_write_right(string_t *dest, const + if (neg) str_append_c(dest,'-'); + acl_rights_write_id(dest, right); + ++ i_assert(acl_id_is_valid(str_c(dest))); + if (strchr(str_c(dest), ' ') != NULL) T_BEGIN { + /* need to escape it */ + const char *escaped = t_strdup(str_escape(str_c(dest))); diff --git a/patches/CVE-2026-42006.patch b/patches/CVE-2026-42006.patch new file mode 100644 index 0000000..9d9c4e0 --- /dev/null +++ b/patches/CVE-2026-42006.patch @@ -0,0 +1,101 @@ +From da1438c76b797f055d4ad7f0eaa17e5e29ca31ee Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 27 Apr 2026 17:40:46 +0300 +Subject: [PATCH] lib-imap: Fix imap_parser_params.list_count_limit to actually + work + +The previous fix in d0f67b52914565a35f3817335ab9633cb291513c was +accidentally limiting the number of ')', not the number of '('. +--- + src/lib-imap/imap-parser.c | 19 +++++++++++-------- + src/lib-imap/test-imap-parser.c | 14 ++++++++++++-- + 2 files changed, 23 insertions(+), 10 deletions(-) + +diff --git a/src/lib-imap/imap-parser.c b/src/lib-imap/imap-parser.c +index 6212aed33d..b1df3d7b78 100644 +--- a/src/lib-imap/imap-parser.c ++++ b/src/lib-imap/imap-parser.c +@@ -191,8 +191,15 @@ static struct imap_arg *imap_arg_create(struct imap_parser *parser) + return arg; + } + +-static void imap_parser_open_list(struct imap_parser *parser) ++static bool imap_parser_open_list(struct imap_parser *parser) + { ++ if (parser->list_count >= parser->list_count_limit) { ++ parser->error_msg = "Too many '('"; ++ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX; ++ return FALSE; ++ } ++ parser->list_count++; ++ + parser->list_arg = imap_arg_create(parser); + parser->list_arg->type = IMAP_ARG_LIST; + p_array_init(&parser->list_arg->_data.list, parser->pool, +@@ -200,6 +207,7 @@ static void imap_parser_open_list(struct imap_parser *parser) + parser->cur_list = &parser->list_arg->_data.list; + + parser->cur_type = ARG_PARSE_NONE; ++ return TRUE; + } + + static bool imap_parser_close_list(struct imap_parser *parser) +@@ -217,12 +225,6 @@ static bool imap_parser_close_list(struct imap_parser *parser) + parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX; + return FALSE; + } +- if (parser->list_count >= parser->list_count_limit) { +- parser->error_msg = "Too many '('"; +- parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX; +- return FALSE; +- } +- parser->list_count++; + + arg = imap_arg_create(parser); + arg->type = IMAP_ARG_EOL; +@@ -673,7 +675,8 @@ static bool imap_parser_read_arg(struct imap_parser *parser) + parser->literal8 = FALSE; + break; + case '(': +- imap_parser_open_list(parser); ++ if (!imap_parser_open_list(parser)) ++ return FALSE; + if ((parser->flags & IMAP_PARSE_FLAG_STOP_AT_LIST) != 0) { + i_stream_skip(parser->input, 1); + return FALSE; +diff --git a/src/lib-imap/test-imap-parser.c b/src/lib-imap/test-imap-parser.c +index fb7c308a23..d509c8176d 100644 +--- a/src/lib-imap/test-imap-parser.c ++++ b/src/lib-imap/test-imap-parser.c +@@ -85,9 +85,15 @@ static void test_imap_parser_list_limit(void) + struct { + const char *input; + int ret; ++ const char *error; + } tests[] = { +- { "(())\r\n", 1 }, +- { "((()))\r\n", -1 }, ++ { "(())\r\n", 1, NULL }, ++ { "((\r\n", -1, "Missing ')'" }, ++ { "(()\r\n", -1, "Missing ')'" }, ++ { "(()))\r\n", -1, "Unexpected ')'" }, ++ { "((()))\r\n", -1, "Too many '('" }, ++ { "(({10}\r\n", -2, NULL }, ++ { "((({10}\r\n", -1, "Too many '('" }, + }; + struct istream_chain *chain; + struct istream *chain_input; +@@ -112,6 +118,10 @@ static void test_imap_parser_list_limit(void) + (void)i_stream_read(chain_input); + + test_assert_cmp(imap_parser_read_args(parser, 0, 0, &args), ==, tests[i].ret); ++ if (tests[i].ret == -1) { ++ enum imap_parser_error err; ++ test_assert_strcmp_idx(imap_parser_get_error(parser, &err), tests[i].error, i); ++ } + /* skip over CRLF */ + i_stream_skip(chain_input, i_stream_get_data_size(chain_input)); + +-- +2.47.3 + diff --git a/patches/Use-_FORTIFY_SOURCE-level-3.patch b/patches/Use-_FORTIFY_SOURCE-level-3.patch new file mode 100644 index 0000000..5fba84a --- /dev/null +++ b/patches/Use-_FORTIFY_SOURCE-level-3.patch @@ -0,0 +1,58 @@ +From: =?utf-8?q?Christian_G=C3=B6ttsche?= +Date: Thu, 22 Dec 2022 17:00:53 +0100 +Subject: Use _FORTIFY_SOURCE level 3 + +Forwarded: not-needed +--- + m4/dovecot.m4 | 6 +++--- + pigeonhole/m4/dovecot.m4 | 6 +++--- + 2 files changed, 6 insertions(+), 6 deletions(-) + +Index: dovecot/m4/dovecot.m4 +=================================================================== +--- dovecot.orig/m4/dovecot.m4 ++++ dovecot/m4/dovecot.m4 +@@ -9,7 +9,7 @@ dnl modifications, as long as this notic + # serial 42 + + dnl +-dnl Check for support for D_FORTIFY_SOURCE=2 ++dnl Check for support for D_FORTIFY_SOURCE=3 + dnl + + AC_DEFUN([AC_CC_D_FORTIFY_SOURCE],[ +@@ -17,8 +17,8 @@ AC_DEFUN([AC_CC_D_FORTIFY_SOURCE],[ + AS_IF([test "$enable_hardening" = yes], [ + case "$host" in + *) +- gl_COMPILER_OPTION_IF([-O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2], [ +- CFLAGS="$CFLAGS -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2" ++ gl_COMPILER_OPTION_IF([-O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3], [ ++ CFLAGS="$CFLAGS -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3" + ], + [], + [AC_LANG_PROGRAM()] +Index: dovecot/pigeonhole/m4/dovecot.m4 +=================================================================== +--- dovecot.orig/pigeonhole/m4/dovecot.m4 ++++ dovecot/pigeonhole/m4/dovecot.m4 +@@ -9,7 +9,7 @@ dnl modifications, as long as this notic + # serial 41 + + dnl +-dnl Check for support for D_FORTIFY_SOURCE=2 ++dnl Check for support for D_FORTIFY_SOURCE=3 + dnl + + AC_DEFUN([AC_CC_D_FORTIFY_SOURCE],[ +@@ -17,8 +17,8 @@ AC_DEFUN([AC_CC_D_FORTIFY_SOURCE],[ + AS_IF([test "$enable_hardening" = yes], [ + case "$host" in + *) +- gl_COMPILER_OPTION_IF([-O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2], [ +- CFLAGS="$CFLAGS -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2" ++ gl_COMPILER_OPTION_IF([-O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3], [ ++ CFLAGS="$CFLAGS -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3" + ], + [], + [AC_LANG_PROGRAM()] diff --git a/patches/acl-Fix-crash-when-group-ACLs-are-used-but-user-s-ac.patch b/patches/acl-Fix-crash-when-group-ACLs-are-used-but-user-s-ac.patch new file mode 100644 index 0000000..c1b0cfb --- /dev/null +++ b/patches/acl-Fix-crash-when-group-ACLs-are-used-but-user-s-ac.patch @@ -0,0 +1,28 @@ +From 003bf9a6959714e0f696f0015c8c712e89962b9b Mon Sep 17 00:00:00 2001 +From: Marco Bettini +Date: Thu, 28 Aug 2025 15:09:56 +0000 +Subject: [PATCH] acl: Fix crash when group ACLs are used, but user's + acl_groups is empty +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1129952 + +--- + src/plugins/acl/acl-backend.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/src/plugins/acl/acl-backend.c b/src/plugins/acl/acl-backend.c +index 2ad901cd96..cc08d702dd 100644 +--- a/src/plugins/acl/acl-backend.c ++++ b/src/plugins/acl/acl-backend.c +@@ -155,7 +155,8 @@ bool acl_backend_user_name_equals(struct acl_backend *backend, + bool acl_backend_user_is_in_group(struct acl_backend *backend, + const char *group_name) + { +- return array_bsearch(&backend->set->acl_groups, group_name, ++ return array_not_empty(&backend->set->acl_groups) && ++ array_bsearch(&backend->set->acl_groups, group_name, + search_strcmp) != NULL; + } + +-- +2.47.3 + diff --git a/patches/auth__Terminate_properly_auth_oauth2_post_setting_defines.patch b/patches/auth__Terminate_properly_auth_oauth2_post_setting_defines.patch new file mode 100644 index 0000000..3ec4715 --- /dev/null +++ b/patches/auth__Terminate_properly_auth_oauth2_post_setting_defines.patch @@ -0,0 +1,25 @@ +From e546239af593dfd0722bea9ff169b60006e219f9 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 15 May 2025 13:06:56 +0300 +Subject: [PATCH] auth: Terminate properly auth_oauth2_post_setting_defines + list + +Fixes: +Error: xoauth2: oauth2 failed: Local validation failed: auth_oauth2_fields settings: Failed to parse configuration: settings struct auth_oauth2_fields #1 key mismatch +--- + src/auth/db-oauth2.c | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/src/auth/db-oauth2.c b/src/auth/db-oauth2.c +index bd807e30b55..230c6cf7678 100644 +--- a/src/auth/db-oauth2.c ++++ b/src/auth/db-oauth2.c +@@ -134,6 +134,8 @@ const struct setting_parser_info auth_oauth2_setting_parser_info = { + static const struct setting_define auth_oauth2_post_setting_defines[] = { + { .type = SET_STRLIST, .key = "oauth2_fields", + .offset = offsetof(struct auth_oauth2_post_settings, fields) }, ++ ++ SETTING_DEFINE_LIST_END + }; + + static const struct auth_oauth2_post_settings auth_oauth2_post_default_settings = { diff --git a/patches/auth__Use_AUTH_CACHE_KEY_USER_instead_of_per-database.patch b/patches/auth__Use_AUTH_CACHE_KEY_USER_instead_of_per-database.patch new file mode 100644 index 0000000..926c1ec --- /dev/null +++ b/patches/auth__Use_AUTH_CACHE_KEY_USER_instead_of_per-database.patch @@ -0,0 +1,124 @@ +From a70ce7d3e2f983979e971414c5892c4e30197231 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Fri, 25 Jul 2025 08:16:52 +0300 +Subject: [PATCH] auth: Use AUTH_CACHE_KEY_USER instead of per-database + constants + +Fixes cache key issue where users would end up overwriting +each other in cache due to cache key being essentially static +string because we no longer support %u. + +Forgotten in 2e298e7ee98b6df61cf85117f000290d60a473b8 +--- + src/auth/auth-settings.h | 2 ++ + src/auth/passdb-bsdauth.c | 4 +--- + src/auth/passdb-oauth2.c | 2 +- + src/auth/passdb-pam.c | 3 ++- + src/auth/passdb-passwd.c | 3 +-- + src/auth/userdb-passwd.c | 3 +-- + 6 files changed, 8 insertions(+), 9 deletions(-) + +diff --git a/src/auth/auth-settings.h b/src/auth/auth-settings.h +index 1d420eceaaf..90aba17ec38 100644 +--- a/src/auth/auth-settings.h ++++ b/src/auth/auth-settings.h +@@ -1,6 +1,8 @@ + #ifndef AUTH_SETTINGS_H + #define AUTH_SETTINGS_H + ++#define AUTH_CACHE_KEY_USER "%{user}" ++ + struct master_service; + struct master_service_settings_output; + +diff --git a/src/auth/passdb-bsdauth.c b/src/auth/passdb-bsdauth.c +index 68292679b7f..1b86da4053c 100644 +--- a/src/auth/passdb-bsdauth.c ++++ b/src/auth/passdb-bsdauth.c +@@ -14,8 +14,6 @@ + #include + #include + +-#define BSDAUTH_CACHE_KEY "%u" +- + struct passdb_bsdauth_settings { + pool_t pool; + }; +@@ -104,7 +102,7 @@ bsdauth_preinit(pool_t pool, struct event *event, + &post_set, error_r) < 0) + return -1; + module->default_cache_key = auth_cache_parse_key_and_fields( +- pool, BSDAUTH_CACHE_KEY, &post_set->fields, "bsdauth"); ++ pool, AUTH_CACHE_KEY_USER, &post_set->fields, "bsdauth"); + + settings_free(post_set); + *module_r = module; +diff --git a/src/auth/passdb-oauth2.c b/src/auth/passdb-oauth2.c +index 96d902d323d..91fed060183 100644 +--- a/src/auth/passdb-oauth2.c ++++ b/src/auth/passdb-oauth2.c +@@ -53,7 +53,7 @@ oauth2_preinit(pool_t pool, struct event *event, struct passdb_module **module_r + if (db_oauth2_init(event, TRUE, &module->db, error_r) < 0) + return -1; + module->module.default_pass_scheme = "PLAIN"; +- module->module.default_cache_key = "%u"; ++ module->module.default_cache_key = AUTH_CACHE_KEY_USER; + *module_r = &module->module; + return 0; + } +diff --git a/src/auth/passdb-pam.c b/src/auth/passdb-pam.c +index 2acbceb80a3..fdf0f573ef4 100644 +--- a/src/auth/passdb-pam.c ++++ b/src/auth/passdb-pam.c +@@ -415,7 +415,8 @@ static int pam_preinit(pool_t pool, struct event *event, + module = p_new(pool, struct pam_passdb_module, 1); + module->module.default_cache_key = + auth_cache_parse_key_and_fields(pool, +- t_strdup_printf("%%u/%s", set->service_name), ++ t_strdup_printf("%"AUTH_CACHE_KEY_USER"\t%s", ++ set->service_name), + &post_set->fields, "pam"); + module->requests_left = set->max_requests; + module->pam_setcred = set->setcred; +diff --git a/src/auth/passdb-passwd.c b/src/auth/passdb-passwd.c +index 13003151f9c..22e2eae7fa3 100644 +--- a/src/auth/passdb-passwd.c ++++ b/src/auth/passdb-passwd.c +@@ -10,7 +10,6 @@ + #include "safe-memset.h" + #include "ipwd.h" + +-#define PASSWD_CACHE_KEY "%u" + #define PASSWD_PASS_SCHEME "CRYPT" + + #undef DEF +@@ -142,7 +141,7 @@ static int passwd_preinit(pool_t pool, struct event *event, + &post_set, error_r) < 0) + return -1; + module->default_cache_key = auth_cache_parse_key_and_fields(pool, +- PASSWD_CACHE_KEY, ++ AUTH_CACHE_KEY_USER, + &post_set->fields, + "passwd"); + settings_free(post_set); +diff --git a/src/auth/userdb-passwd.c b/src/auth/userdb-passwd.c +index 5241129a0cc..14cf90a6d65 100644 +--- a/src/auth/userdb-passwd.c ++++ b/src/auth/userdb-passwd.c +@@ -9,7 +9,6 @@ + #include "ipwd.h" + #include "time-util.h" + +-#define USER_CACHE_KEY "%u" + #define PASSWD_SLOW_WARN_MSECS (10*1000) + #define PASSWD_SLOW_MASTER_WARN_MSECS 50 + #define PASSDB_SLOW_MASTER_WARN_COUNT_INTERVAL 100 +@@ -225,7 +224,7 @@ static int passwd_preinit(pool_t pool, struct event *event ATTR_UNUSED, + struct passwd_userdb_module *module = + p_new(pool, struct passwd_userdb_module, 1); + +- module->module.default_cache_key = USER_CACHE_KEY; ++ module->module.default_cache_key = AUTH_CACHE_KEY_USER; + *module_r = &module->module; + return 0; + } diff --git a/patches/bug1104549-gssapi-regression.patch b/patches/bug1104549-gssapi-regression.patch new file mode 100644 index 0000000..d95115c --- /dev/null +++ b/patches/bug1104549-gssapi-regression.patch @@ -0,0 +1,21 @@ +Description: Fix GSSAPI regression + Dovecot 2.4 introduced a regression that broke GSSAPI authentication for + some clients. This patch contains a fix provided by the upstream maintainers. +Origin: https://dovecot.org/mailman3/archives/list/dovecot@dovecot.org/message/O54EAGLIXXHMOH7BQCCKHHB3Z32HDWVR/ +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1104549 +Last-Update: 2025-05-02 +--- +This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ +Index: dovecot/src/auth/mech-gssapi.c +=================================================================== +--- dovecot.orig/src/auth/mech-gssapi.c ++++ dovecot/src/auth/mech-gssapi.c +@@ -672,7 +672,7 @@ mech_gssapi_auth_initial(struct auth_req + + if (data_size == 0) { + /* The client should go first */ +- auth_request_handler_reply_continue(request, NULL, 0); ++ auth_request_handler_reply_continue(request, uchar_empty_ptr, 0); + } else { + mech_gssapi_auth_continue(request, data, data_size); + } diff --git a/patches/bug1106784_Fix-LDAP-SASL-auth-support.patch b/patches/bug1106784_Fix-LDAP-SASL-auth-support.patch new file mode 100644 index 0000000..d032ccd --- /dev/null +++ b/patches/bug1106784_Fix-LDAP-SASL-auth-support.patch @@ -0,0 +1,39 @@ +From 09cc148e4bbbae91514b6ff58bd71c15f95e4e8a Mon Sep 17 00:00:00 2001 +From: Jakob Haufe +Date: Sun, 25 May 2025 15:04:50 +0200 +Subject: [PATCH] Fix LDAP SASL auth support + +961275fdb54878fdfa4ee1b9f1a4f00e82bf4a83 moved code without creating a +way to have HAVE_LDAP_SASL defined there. + +Copy the preprocessor block from src/auth/db-ldap.c to fix this. +--- + src/auth/db-ldap-settings.c | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +diff --git a/src/auth/db-ldap-settings.c b/src/auth/db-ldap-settings.c +index 6c8feb334e..9245fcabd2 100644 +--- a/src/auth/db-ldap-settings.c ++++ b/src/auth/db-ldap-settings.c +@@ -137,6 +137,18 @@ const struct setting_parser_info ldap_post_setting_parser_info = { + + /* */ + ++#define HAVE_LDAP_SASL ++#ifdef HAVE_SASL_SASL_H ++# include ++#elif defined (HAVE_SASL_H) ++# include ++#else ++# undef HAVE_LDAP_SASL ++#endif ++#if !defined(SASL_VERSION_MAJOR) || SASL_VERSION_MAJOR < 2 ++# undef HAVE_LDAP_SASL ++#endif ++ + static int ldap_parse_deref(const char *str, int *ref_r) + { + if (strcasecmp(str, "never") == 0) +-- +2.47.2 + diff --git a/patches/bug1121000_dovecot-ldap_Crash_if_iterate_filter_is_set_but_iterate_fields_is_not_set.patch b/patches/bug1121000_dovecot-ldap_Crash_if_iterate_filter_is_set_but_iterate_fields_is_not_set.patch new file mode 100644 index 0000000..776e669 --- /dev/null +++ b/patches/bug1121000_dovecot-ldap_Crash_if_iterate_filter_is_set_but_iterate_fields_is_not_set.patch @@ -0,0 +1,28 @@ +From 576a2f52bff4c13971d9e6d1172857a4f18ddd14 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Thu, 6 Nov 2025 14:52:37 +0200 +Subject: [PATCH] auth: ldap - Fix crash if users are iterated, but + userdb_ldap_iterate_fields is not set +Bug-Debian: https://bugs.debian.org/1121000 +--- + src/auth/userdb-ldap.c | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) + +diff --git a/src/auth/userdb-ldap.c b/src/auth/userdb-ldap.c +index 7d6d2bc38a..7aad2db74f 100644 +--- a/src/auth/userdb-ldap.c ++++ b/src/auth/userdb-ldap.c +@@ -201,8 +201,11 @@ static void userdb_ldap_iterate_callback(struct ldap_connection *conn, + &set, &error) < 0) { + e_error(event, "%s", error); + ctx->ctx.failed = TRUE; +- } +- else { ++ } else if (!array_is_created(&set->iterate_fields)) { ++ e_error(event, "iterate: No userdb_ldap_iterate_fields specified"); ++ ctx->ctx.failed = TRUE; ++ settings_free(set); ++ } else { + unsigned int count; + const char *const *items = array_get(&set->iterate_fields, &count); + for (unsigned int ndx = 0; ndx < count - 1;) { diff --git a/patches/fit-32-bit-test-integers.patch b/patches/fit-32-bit-test-integers.patch new file mode 100644 index 0000000..3238f76 --- /dev/null +++ b/patches/fit-32-bit-test-integers.patch @@ -0,0 +1,61 @@ +Index: dovecot/src/lib/test-event-filter.c +=================================================================== +--- dovecot.orig/src/lib/test-event-filter.c ++++ dovecot/src/lib/test-event-filter.c +@@ -1003,32 +1003,32 @@ static void test_event_filter_interval_v + { "field > 1mins", 60 * 1000 * 1000, FALSE }, + { "field < 1mins", 60 * 1000 * 1000, FALSE }, + +- { "field = 1hours", 60L * 60 * 1000 * 1000, TRUE }, +- { "field = 1h", 60L * 60 * 1000 * 1000, TRUE }, +- { "field = 3600000000", 60L * 60 * 1000 * 1000, TRUE }, +- { "field >= 1hours", 60L * 60 * 1000 * 1000, TRUE }, +- { "field <= 1hours", 60L * 60 * 1000 * 1000, TRUE }, +- { "field > 1mins", 60L * 60 * 1000 * 1000, TRUE }, +- { "field > 1hours", 60L * 60 * 1000 * 1000, FALSE }, +- { "field < 1hours", 60L * 60 * 1000 * 1000, FALSE }, ++ { "field = 1hours", 60LL * 60 * 1000 * 1000, TRUE }, ++ { "field = 1h", 60LL * 60 * 1000 * 1000, TRUE }, ++ { "field = 3600000000", 60LL * 60 * 1000 * 1000, TRUE }, ++ { "field >= 1hours", 60LL * 60 * 1000 * 1000, TRUE }, ++ { "field <= 1hours", 60LL * 60 * 1000 * 1000, TRUE }, ++ { "field > 1mins", 60LL * 60 * 1000 * 1000, TRUE }, ++ { "field > 1hours", 60LL * 60 * 1000 * 1000, FALSE }, ++ { "field < 1hours", 60LL * 60 * 1000 * 1000, FALSE }, + +- { "field = 1days", 24L * 60 * 60 * 1000 * 1000, TRUE }, +- { "field = 1d", 24L * 60 * 60 * 1000 * 1000, TRUE }, +- { "field = 86400000000", 24L * 60 * 60 * 1000 * 1000, TRUE }, +- { "field >= 1days", 24L * 60 * 60 * 1000 * 1000, TRUE }, +- { "field <= 1days", 24L * 60 * 60 * 1000 * 1000, TRUE }, +- { "field > 1hours", 24L * 60 * 60 * 1000 * 1000, TRUE }, +- { "field > 1days", 24L * 60 * 60 * 1000 * 1000, FALSE }, +- { "field < 1days", 24L * 60 * 60 * 1000 * 1000, FALSE }, ++ { "field = 1days", 24LL * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field = 1d", 24LL * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field = 86400000000", 24LL * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field >= 1days", 24LL * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field <= 1days", 24LL * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field > 1hours", 24LL * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field > 1days", 24LL * 60 * 60 * 1000 * 1000, FALSE }, ++ { "field < 1days", 24LL * 60 * 60 * 1000 * 1000, FALSE }, + +- { "field = 1weeks", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE }, +- { "field = 1w", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE }, +- { "field = 604800000000", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE }, +- { "field >= 1weeks", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE }, +- { "field <= 1weeks", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE }, +- { "field > 1days", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE }, +- { "field > 1weeks", 7L * 24 * 60 * 60 * 1000 * 1000, FALSE }, +- { "field < 1weeks", 7L * 24 * 60 * 60 * 1000 * 1000, FALSE }, ++ { "field = 1weeks", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field = 1w", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field = 604800000000", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field >= 1weeks", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field <= 1weeks", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field > 1days", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE }, ++ { "field > 1weeks", 7LL * 24 * 60 * 60 * 1000 * 1000, FALSE }, ++ { "field < 1weeks", 7LL * 24 * 60 * 60 * 1000 * 1000, FALSE }, + }; + + struct event_filter *filter; diff --git a/patches/fix-man-errors.patch b/patches/fix-man-errors.patch new file mode 100644 index 0000000..88c8de6 --- /dev/null +++ b/patches/fix-man-errors.patch @@ -0,0 +1,72 @@ +Description: Fix groff errors in upstream manpages +Author: Noah Meyerhans +Forwarded: no +Last-Update: 2025-05-02 +--- +This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ +Index: dovecot/doc/man/dovecot-lda.1 +=================================================================== +--- dovecot.orig/doc/man/dovecot-lda.1 ++++ dovecot/doc/man/dovecot-lda.1 +@@ -1,5 +1,6 @@ +-.TH "" "" "March 2025" "78ffb79" "Dovecot" +-dovecot-lda - Dovecot's local mail delivery agent ++.TH "DOVECOT-LDA" "1" "March 2025" "78ffb79" "Dovecot" ++.SH "NAME" ++\fBdovecot-lda\fR - Dovecot's local mail delivery agent + .SH "SYNOPSIS" + .P + \fBdovecot-lda\fR \[lB]\fB-ek\fR\[rB] \[lB]\fB-a\fR \fIaddress\fR\[rB] \[lB]\fB-c\fR \fIconfig_file\fR\[rB] \[lB]\fB-d\fR \fIusername\fR\[rB] \[lB]\fB-f\fR \fIenvelope_sender\fR\[rB] \[lB]\fB-m\fR \fImailbox\fR\[rB] \[lB]\fB-o\fR \fIsetting=value\fR\[rB] \[lB]\fB-p\fR \fIpath\fR\[rB] \[lB]\fB-r\fR \fIaddress\fR\[rB] +Index: dovecot/doc/man/doveadm-deduplicate.1 +=================================================================== +--- dovecot.orig/doc/man/doveadm-deduplicate.1 ++++ dovecot/doc/man/doveadm-deduplicate.1 +@@ -6,7 +6,7 @@ + \fBdoveadm\fR \[lB]\fIGLOBAL OPTIONS\fR\[rB] \fBdeduplicate\fR \[lB]\fB-u\fR \fIuser\fR | \fB-A\fR | \fB-F\fR \fIfile\fR | \fB--no-userdb-lookup\fR\[rB] \[lB]\fB-S\fR \fIsocket_path\fR\[rB] \[lB]\fB-m\fR\[rB] \fIsearch_query\fR + .SH "DESCRIPTION" + .P +-. doveadm(1) will delete the newest duplicated messages from the mailbox and keep the oldest. ++doveadm(1) will delete the newest duplicated messages from the mailbox and keep the oldest. + .P + Deduplication across multiple mailboxes is not supported. + .SH "GLOBAL OPTIONS" +@@ -157,7 +157,7 @@ Run the \fIcommand\fR only for the given + .RS 0 + .RS 4 + .P +-' will be sufficient. See doveadm-search-query(7) for details. ++will be sufficient. See doveadm-search-query(7) for details. + .RE 0 + + .RE 0 +@@ -179,7 +179,7 @@ guid uid + 8aad0f0a30169f4bea620000ca356bad 18756 + 923e301ab9219b4b4f440000ca356bad 18748 + 923e301ab9219b4b4f440000ca356bad 18753 +-... ++\fR... + .fi + .RE + .P +@@ -196,7 +196,7 @@ guid uid + 8aad0f0a30169f4bea620000ca356bad 18751 + 923e301ab9219b4b4f440000ca356bad 18748 + a7999e1530739c4bd26d0000ca356bad 18749 +-... ++\fR... + .fi + .RE + .SH "REPORTING BUGS" +Index: dovecot/doc/man/doveadm-fetch.1 +=================================================================== +--- dovecot.orig/doc/man/doveadm-fetch.1 ++++ dovecot/doc/man/doveadm-fetch.1 +@@ -377,7 +377,7 @@ The date-time attribute when present, ot + .RS 0 + .RS 4 + .P +-Fetch messages matching this search query. See doveadm-search-query(1) for details. ++Fetch messages matching this search query. See doveadm-search-query(7) for details. + .RE 0 + + .RE 0 diff --git a/patches/lib-mail-istream-header-filter-Fix-potential-assert-.patch b/patches/lib-mail-istream-header-filter-Fix-potential-assert-.patch new file mode 100644 index 0000000..6cb6c9c --- /dev/null +++ b/patches/lib-mail-istream-header-filter-Fix-potential-assert-.patch @@ -0,0 +1,84 @@ +From 3ff6022baa757583faf579251fc4033f373d4244 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 2 Sep 2025 13:28:44 +0300 +Subject: [PATCH] lib-mail: istream-header-filter - Fix potential assert-crash + +In some situations multiple snapshots were created, which broke the logic +of using a single snapshot_pending boolean. Changed it to refcount. + +Fixes: +Panic: file istream-header-filter.c: line 665 (i_stream_header_filter_snapshot_free): assertion failed: (snapshot->mstream->snapshot_pending) +--- + src/lib-mail/istream-header-filter.c | 14 +++++++------- + 1 file changed, 7 insertions(+), 7 deletions(-) + +diff --git a/src/lib-mail/istream-header-filter.c b/src/lib-mail/istream-header-filter.c +index ee94e4f82b..2d938bdaad 100644 +--- a/src/lib-mail/istream-header-filter.c ++++ b/src/lib-mail/istream-header-filter.c +@@ -27,6 +27,7 @@ struct header_filter_istream { + void *context; + + buffer_t *hdr_buf; ++ int snapshot_pending_refcount; + struct message_size header_size; + uoff_t skip_count; + uoff_t last_lf_offset; +@@ -50,7 +51,6 @@ struct header_filter_istream { + bool eoh_not_matched:1; + bool callbacks_called:1; + bool prev_matched:1; +- bool snapshot_pending:1; + }; + + header_filter_callback *null_header_filter_callback = NULL; +@@ -66,7 +66,7 @@ static void i_stream_header_filter_destroy(struct iostream_private *stream) + message_parse_header_deinit(&mstream->hdr_ctx); + if (array_is_created(&mstream->match_change_lines)) + array_free(&mstream->match_change_lines); +- if (!mstream->snapshot_pending) ++ if (mstream->snapshot_pending_refcount == 0) + buffer_free(&mstream->hdr_buf); + else { + /* Clear hdr_buf to make sure +@@ -168,7 +168,7 @@ static ssize_t hdr_stream_update_pos(struct header_filter_istream *mstream) + + static void hdr_buf_realloc_if_needed(struct header_filter_istream *mstream) + { +- if (!mstream->snapshot_pending) ++ if (mstream->snapshot_pending_refcount == 0) + return; + + /* hdr_buf exists in a snapshot. Leave it be and create a copy of it +@@ -177,7 +177,7 @@ static void hdr_buf_realloc_if_needed(struct header_filter_istream *mstream) + mstream->hdr_buf = buffer_create_dynamic(default_pool, + I_MAX(1024, old_buf->used)); + buffer_append(mstream->hdr_buf, old_buf->data, old_buf->used); +- mstream->snapshot_pending = FALSE; ++ mstream->snapshot_pending_refcount = 0; + + mstream->istream.buffer = mstream->hdr_buf->data; + } +@@ -662,8 +662,8 @@ i_stream_header_filter_snapshot_free(struct istream_snapshot *_snapshot) + if (snapshot->mstream->hdr_buf != snapshot->hdr_buf) + buffer_free(&snapshot->hdr_buf); + else { +- i_assert(snapshot->mstream->snapshot_pending); +- snapshot->mstream->snapshot_pending = FALSE; ++ i_assert(snapshot->mstream->snapshot_pending_refcount > 0); ++ snapshot->mstream->snapshot_pending_refcount--; + } + i_free(snapshot); + } +@@ -687,7 +687,7 @@ i_stream_header_filter_snapshot(struct istream_private *stream, + snapshot->hdr_buf = mstream->hdr_buf; + snapshot->snapshot.free = i_stream_header_filter_snapshot_free; + snapshot->snapshot.prev_snapshot = prev_snapshot; +- mstream->snapshot_pending = TRUE; ++ mstream->snapshot_pending_refcount++; + return &snapshot->snapshot; + } + +-- +2.47.3 + diff --git a/patches/lib-sieve_sieve-script_c_sieve_script_create_common_Correctly_handle_errors.patch b/patches/lib-sieve_sieve-script_c_sieve_script_create_common_Correctly_handle_errors.patch new file mode 100644 index 0000000..40d1e17 --- /dev/null +++ b/patches/lib-sieve_sieve-script_c_sieve_script_create_common_Correctly_handle_errors.patch @@ -0,0 +1,26 @@ +From 2b9a4e85bc4547fcff84398b2bda00d0ced09fae Mon Sep 17 00:00:00 2001 +From: Alexander Gerasiov +Date: Tue, 23 Sep 2025 13:50:43 +0300 +Subject: [PATCH] lib-sieve/sieve-script.c: sieve_script_create_common: + Correctly handle errors. + +Fixes null pointer deref (e.g. in case of absent file). +--- + src/lib-sieve/sieve-script.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +Index: dovecot/pigeonhole/src/lib-sieve/sieve-script.c +=================================================================== +--- dovecot.orig/pigeonhole/src/lib-sieve/sieve-script.c ++++ dovecot/pigeonhole/src/lib-sieve/sieve-script.c +@@ -166,8 +166,8 @@ sieve_script_create_common(struct sieve_ + sieve_storage_unref(&storage); + if (ret > 0 && open && + sieve_script_open(script, error_code_r) < 0) { +- *error_r = sieve_storage_get_last_error( +- storage, error_code_r); ++ *error_r = sieve_script_get_last_error( ++ script, error_code_r); + sieve_script_unref(&script); + if (*error_code_r == SIEVE_ERROR_NOT_FOUND) + continue; diff --git a/patches/series b/patches/series new file mode 100644 index 0000000..328862f --- /dev/null +++ b/patches/series @@ -0,0 +1,212 @@ +#split-protocols.patch +#fix-mail_plugin_dir-default.patch +#ssl-cert-location.patch +#tcpwrapper.patch +#default-mail_location.patch +#mboxlocking.patch +#dovecot_name.patch +#ssl-dh-params-location.patch +skip-rfc-subdir.patch +#Correct-misspellings.patch +#test-backtrace.patch +#doveadm-director.1-drop-acute-accent.patch +#Fix-32bit-sign-comparisons.patch +#Improve-cross-compile-support.patch +#Silence-LTO-related-compiler-warning.patch +#md4-md5-disable-optimization-causing-unaligned-access.patch +#Support-openssl-3.0.patch +#Debug-flaky-unit-test.patch +#auth-Fix-handling-passdbs-with-identical-driver-args-but-.patch +#auth-Add-a-comment-about-updating-userdb_find.patch +#Fix-uninitialized-read-in-doveadm-oldstats.patch +0001-lda-Fix-using-USER-environment-if-d-hasn-t-been-spec.patch +0002-lda-Default-mail_home-HOME-environment-if-not-using-.patch +Use-_FORTIFY_SOURCE-level-3.patch +fit-32-bit-test-integers.patch +bug1104549-gssapi-regression.patch +fix-man-errors.patch +bug1106784_Fix-LDAP-SASL-auth-support.patch +auth__Use_AUTH_CACHE_KEY_USER_instead_of_per-database.patch +auth__Terminate_properly_auth_oauth2_post_setting_defines.patch +lib-sieve_sieve-script_c_sieve_script_create_common_Correctly_handle_errors.patch +bug1121000_dovecot-ldap_Crash_if_iterate_filter_is_set_but_iterate_fields_is_not_set.patch +0001-trash-Use-mailbox-event-in-trash_try_mailbox-for-set.patch +acl-Fix-crash-when-group-ACLs-are-used-but-user-s-ac.patch +CVE-2025-59028.patch +CVE-2025-59031.patch +CVE-2025-59032.patch +CVE-2026-24031-27860-1.patch +CVE-2026-24031-27860-2.patch +CVE-2026-24031-27860-3.patch +CVE-2026-24031-27860-4.patch +CVE-2026-24031-27860-5.patch +CVE-2026-24031-27860-6.patch +CVE-2026-24031-27860-7.patch +CVE-2026-24031-27860-8.patch +CVE-2026-27855-1.patch +CVE-2026-27855-2.patch +CVE-2026-27855-3.patch +CVE-2026-27855-4.patch +CVE-2026-27856-1.patch +CVE-2026-27856-2.patch +CVE-2026-27856-3.patch +CVE-2026-27857-1.patch +CVE-2026-27857-2.patch +CVE-2026-27857-3.patch +CVE-2026-27857-4.patch +CVE-2026-27857-5.patch +CVE-2026-27858.patch +CVE-2026-27859.patch +CVE-2026-27851.patch +CVE-2026-40016.patch +CVE-2026-33603.patch +CVE-2026-40020-1.patch +CVE-2026-40020-2.patch +CVE-2026-40020-3.patch +CVE-2026-42006.patch +lib-mail-istream-header-filter-Fix-potential-assert-.patch +# CVE-2026-52687 +0001-imap-Restrict-COMPRESS-to-DEFLATE-only.patch +0002-doveadm-compress-connect-Don-t-hang-when-server-reje.patch +0003-doveadm-compress-Delay-failing-if-used-COMPRESS-algo.patch +# CVE-2026-52681 +0001-lib-storage-Allow-NULL-path-in-mailbox_list_mkdir_ro.patch +0002-lib-storage-Simplify-mailbox_mkdir-using-mailbox_lis.patch +0001-lib-sieve-sieve-storage-Add-is_personal-bit.patch +0001-lib-sieve-sieve-storage-Fix-panic-caused-by-flawed-d.patch +0001-lib-sieve-Add-per-user-sieve-rusage-file-for-cumulat.patch +0002-lib-sieve-Drop-binary-header-resource-usage-tracking.patch +0003-lib-sieve-Rename-on-disk-header-rusage-fields-to-unu.patch +0004-lib-sieve-Switch-sieve_max_cpu_time-enforcement-to-t.patch +0005-lib-sieve-file-storage-Remove-orphan-compiled-binari.patch +0006-lib-sieve-Log-a-warning-when-cumulative-CPU-exceeds-.patch +0007-lib-sieve-Log-an-info-line-when-opening-a-CPU-disabl.patch +0008-lib-sieve-Reduce-CPU-limit-check-interval-in-contain.patch +# CVE-2026-42391 +0001-imap-login-Add-comments-to-internal-ID-command-param.patch +0001-imap-login-Fix-excessive-memory-growth-with-pre-logi.patch +# CVE-2026-40204 +0001-lib-lda-mail-deliver-Autocreate-delivery-mailbox-via.patch +0002-lib-storage-Remove-MAILBOX_FLAG_AUTO_CREATE-and-MAIL.patch +0001-lib-sieve-actions-Split-mailbox-creation-into-sieve_.patch +0002-lib-sieve-actions-Create-fileinto-mailbox-without-th.patch +# CVE-2026-42394 +0001-lib-i_close_fd-Document-that-it-preserves-errno.patch +0002-lib-path-util-Add-t_openat_safe-for-jailed-openat-re.patch +0003-lib-Add-t_openat_safe_dir.patch +0004-acl-dovecot-acl-list-is-never-supposed-to-be-a-symli.patch +0005-lib-Add-comments-about-memory-allocations-and-string.patch +0001-lib-sieve-storage-file-Refuse-symlinks-escaping-pers.patch +0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch +# CVE-2026-73209 +0004-lib-compression-test-Add-zero-len-frame-test-for-all.patch +0003-lib-compression-istream-zstd-Guard-against-no-progre.patch +0001-lib-compression-istream-lz4-Ensure-uncompressed-chun.patch +0001-lib-compression-istream-lz4-Try-again-if-no-data-was.patch +0001-lib-compression-Don-t-calculate-crc32-if-using-zlib-.patch +0001-lib-compression-istream-zlib-Use-container_of-macro.patch +0001-global-Fix-spelling.patch +0001-lib-compression-wrap-_read-in-for-loop-reindent-only.patch +0002-lib-compression-Iterate-instead-of-recursing-on-zero.patch +# CVE-2026-40018 +0001-lib-sql-sql-api-Implement-async-calls-for-drivers-th.patch +0001-lib-sql-Add-sql_result_new_error-helper.patch +0002-lib-sql-Extract-sql_query_callback_delayed-helper.patch +0003-lib-sql-Add-sql_commit_schedule_delayed-helper.patch +0004-lib-var-expand-Change-escape-func-signature-to-retur.patch +0001-lib-sql-driver-sqlite-Use-sqlite3_snprintf-to-quote-.patch +0001-lib-sql-Use-strchr-based-while-loop-to-fill-template.patch +0005-lib-sql-Make-escape_string-return-int-with-error_r-f.patch +0006-auth-passdb_sql-connect-before-expanding-query-varia.patch +0007-lib-sql-Run-sql-statement-queries-in-their-own-data-.patch +# CVE-2026-40205 and CVE-2026-73208 +0001-auth-db-oauth2-Reduce-nesting-in-token_in_scope-via-.patch +0002-auth-db-oauth2-Fix-scope-check-to-require-all-config.patch +0003-auth-db-oauth2-Add-oauth2_audience-setting-deprecate.patch +# CVE-2026-33604 +0001-lib-mail-ostream-dot-Optimize-stream-writing.patch +0001-lib-mail-o_stream_dot_sendv-Use-I_MIN.patch +0002-lib-mail-o_stream_dot_sendv-Fix-boundary-off-by-one.patch +0003-lib-mail-o_stream_dot_sendv-Use-enumeration-to-defin.patch +0001-lib-test-Add-test_assert_memcmp.patch +0001-lib-test-Require-both-lengths-in-test_assert_memcmp.patch +0004-lib-mail-o_stream_dot_sendv-Do-not-send-unguarded-.-.patch +# CVE-2026-33607 +0001-lib-imap-imap-match-Fix-excessive-CPU-usage-caused-b.patch +# CVE-2026-27852 +0001-lib-mail-Reset-charset-translation-buffer-between-MI.patch +0002-imap-Stream-BODYSTRUCTURE-to-client-without-ostream-.patch +0003-imap-Stream-ENVELOPE-to-client-without-ostream-memor.patch +0004-lib-mail-message-parser-from-parts-Enforce-50-MB-all.patch +0005-lib-mail-Add-count-field-to-struct-message_address_l.patch +0006-lib-storage-mbox-Explicitly-disable-the-header-block.patch +0007-lib-mail-istream-header-filter-Use-container_of-for-.patch +0008-lib-mail-Make-istream-header-filter-s-per-header-siz.patch +0009-lib-storage-Cap-per-header-size-in-index_mail_get_he.patch +0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch +0011-lib-mail-Introduce-struct-message_part_data_limits-a.patch +0012-lib-mail-Limit-total-address-count-per-message-to-10.patch +0013-lib-mail-Limit-total-Content-Language-tag-count-per-.patch +0014-lib-mail-Limit-total-MIME-parameter-count-per-messag.patch +# CVE-2026-40014 +0001-lib-storage-thread-Avoid-excessive-data-stack-growth.patch +0002-lib-storage-thread-Limit-References-header-msgid-cou.patch +0003-lib-storage-thread-Limit-ancestor-chain-traversal-de.patch +# CVE-2026-40013 +0001-lib-sieve-sieve-binary-code-Fix-single-byte-oob-stac.patch +# CVE-2026-33605 +0001-lib-managesieve-managesieve-parser-Fix-handling-of-l.patch +0002-managesieve-login-client_skip_line-Discard-data-when.patch +# CVE-2026-40015 +0001-imap-hibernate-Fix-out-of-bounds-read-when-parsing-D.patch +0002-imap-hibernate-Use-imap-parser-API-for-parsing-DONE-.patch +# CVE-2026-40017 +0001-lib-Add-XXH64-hash-implementation.patch +0001-lib-xxh64-fix-byte-ordering-bug-on-big-endian-system.patch +0002-lib-Replace-str_hash-strcase_hash-with-xxh64.patch +0003-lib-Add-str_stable_hash.patch +0004-lib-Key-str_hash-strcase_hash-with-a-random-seed-to-.patch +0005-lib-index-mail-index-strmap-Fix-OOB-read-from-trunca.patch +0006-lib-index-Rename-crc32-variables-parameters-to-hash-.patch +0007-lib-index-Add-keyed-xxh64-strmap-format-v2-gated-by-.patch +0008-doveadm-Fix-ubsan-unsigned-integer-overflow-error.patch +0009-config-Assume-dovecot_config_version-0.0.0-is-the-sa.patch +0010-lib-master-Move-config_version_find-from-src-config-.patch +0011-lib-lib-master-Move-version_-to-lib-version.-ch.patch +0012-config-doveconf-dF-Add-dovecot_storage_version.patch +# CVE-2026-42007 +0001-lib-sieve-util-edit-mail-Fix-writing-to-freed-memory.patch +0002-lib-sieve-util-test-edit-mail-Adjust-to-changes-in-d.patch +0003-lib-sieve-edit_mail_headers_parse-Fix-info-leak-via-.patch +# CVE-2026-33263 +0001-login-common-client-common-Add-client_disconnect-vfu.patch +0002-submission-login-client-Fix-panic-occurring-at-mail_.patch +0003-lib-smtp-smtp-server-Expose-smtp_server_connection_r.patch +0004-submission-login-client-authenticate-Reply-421-4.7.0.patch +0001-commin-Rename-LOGIN_PROXY_FAILURE_TYPE_AUTH-to-LOGIN.patch +0001-managesieve-login-Adjust-to-core-s-login_proxy_failu.patch +0001-login-common-login-Add-proxy_dest_connection_limit-e.patch +0005-submission-login-submission-proxy-Recognize-421-4.7..patch +0001-managesieve-login-Support-LOGIN_PROXY_FAILURE_TYPE_A.patch +0001-login-Add-LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED.patch +# CVE-2026-33606 +0001-dsync-Avoid-potentially-excessive-data-stack-growth-.patch +0002-dsync-Fix-escaping-mail-or-attribute-value-that-begi.patch +# CVE-2026-40203 +0001-imap-Extract-side-channel-ostream-creation-into-help.patch +0002-imap-Recreate-multiplex-ostream-side-channel-after-u.patch +0003-lib-compression-Add-o_stream_deflate_reset_dict.patch +0004-imap-login-Add-dict_reset-side-channel-command-to-im.patch +# CVE-2026-42008 +0005-imap-Add-imap_compress_on_proxy-hidden-setting.patch +0006-imap-Reset-DEFLATE-dictionary-after-every-tagged-rep.patch +0001-submission-Fix-settings-leak-on-error-paths-in-clien.patch +0002-auth-Fix-prefixing-forward_fields-without-a-value-fr.patch +# CVE-2026-42392 +0001-lib-imap-urlauth-Fix-leaking-uninitialized-memory-in.patch +# CVE-2026-42393 +0001-lib-Add-str_equals_timing_safe.patch +0002-doveadm-Avoid-leaking-doveadm_password-or-doveadm_ap.patch +# CVE-2026-42395 +0001-login-common-Fix-crash-when-XCLIENT-FORWARD-base64-c.patch +0002-login-common-Avoid-array_front-panic-on-empty-forwar.patch diff --git a/patches/skip-rfc-subdir.patch b/patches/skip-rfc-subdir.patch new file mode 100644 index 0000000..5f35dc0 --- /dev/null +++ b/patches/skip-rfc-subdir.patch @@ -0,0 +1,34 @@ +From: Noah Meyerhans +Date: Thu, 21 May 2020 21:48:59 -0700 +Subject: Don't try to build doc/rfc subdir components +Forwarded: not-needed +--- + pigeonhole/configure.ac | 1 - + pigeonhole/doc/Makefile.am | 1 - + pigeonhole/doc/Makefile.in | 1 - + 3 files changed, 3 deletions(-) + +Index: dovecot/pigeonhole/configure.ac +=================================================================== +--- dovecot.orig/pigeonhole/configure.ac ++++ dovecot/pigeonhole/configure.ac +@@ -258,7 +258,6 @@ doc/Makefile + doc/man/Makefile + doc/example-config/Makefile + doc/example-config/conf.d/Makefile +-doc/rfc/Makefile + doc/extensions/Makefile + doc/locations/Makefile + doc/plugins/Makefile +Index: dovecot/pigeonhole/doc/Makefile.am +=================================================================== +--- dovecot.orig/pigeonhole/doc/Makefile.am ++++ dovecot/pigeonhole/doc/Makefile.am +@@ -1,7 +1,6 @@ + SUBDIRS = \ + man \ + example-config \ +- rfc \ + extensions \ + locations \ + plugins diff --git a/po/nl.po b/po/nl.po new file mode 100644 index 0000000..e5c0446 --- /dev/null +++ b/po/nl.po @@ -0,0 +1,102 @@ +# Dutch translation of dovecot debconf templates. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the dovecot package. +# Frans Spiesschaert , 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: dovecot\n" +"Report-Msgid-Bugs-To: dovecot@packages.debian.org\n" +"POT-Creation-Date: 2013-11-07 07:06+0100\n" +"PO-Revision-Date: 2014-10-04 22:25+0200\n" +"Last-Translator: Frans Spiesschaert \n" +"Language-Team: Debian Dutch l10n Team \n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Type: error +#. Description +#: ../dovecot-core.templates:2001 +msgid "Wrong location for SSL certificates" +msgstr "De SSL-certificaten staan op een verkeerde plaats" + +#. Type: error +#. Description +#: ../dovecot-core.templates:2001 +msgid "" +"This machine uses SSL certificates for Dovecot. These certificates should be " +"moved from /etc/ssl to /etc/dovecot and Dovecot's configuration file (/etc/" +"dovecot/conf.d/10-ssl.conf) should be updated accordingly." +msgstr "" +"Deze computer gebruikt SSL-certificaten voor Dovecot. Deze certificaten " +"moeten verplaatst worden van /etc/ssl naar /etc/dovecot en het " +"configuratiebestand van Dovecot (/etc/dovecot/conf.d/10-ssl.conf) moet in " +"die zin aangepast worden." + +#. Type: error +#. Description +#: ../dovecot-core.templates:2001 +msgid "Please read /usr/share/doc/dovecot-core/README.Debian.gz for details." +msgstr "" +"Raadpleeg /usr/share/doc/dovecot-core/README.Debian.gz voor de details." + +#. Type: boolean +#. Description +#: ../dovecot-core.templates:3001 +msgid "Create a self-signed SSL certificate?" +msgstr "Een door uzelf ondertekend SSL-certificaat aanmaken?" + +#. Type: boolean +#. Description +#: ../dovecot-core.templates:3001 +msgid "" +"An SSL certificate is needed in order to use IMAP or POP3 over SSL/TLS. No " +"such certificate was found." +msgstr "" +"U heeft een SSL-certificaat nodig om IMAP of POP3 over SSL/TLS te kunnen " +"gebruiken. Een dergelijk certificaat werd niet gevonden." + +#. Type: boolean +#. Description +#: ../dovecot-core.templates:3001 +msgid "" +"Please choose whether you want to create one now. This will then be a self-" +"signed certificate." +msgstr "" +"Geef aan of u nu een dergelijk certificaat wilt aanmaken. In dat geval zal " +"het een door uzelf ondertekend certificaat zijn." + +#. Type: boolean +#. Description +#: ../dovecot-core.templates:3001 +msgid "" +"If you choose not to create a certificate, please adapt Dovecot's " +"configuration file (/etc/dovecot/conf.d/10-ssl.conf)." +msgstr "" +"Indien u ervoor kiest om geen certificaat aan te maken, dan moet u het " +"configuratiebestand van Dovecot (/etc/dovecot/conf.d/10-ssl.conf) wijzigen." + +#. Type: string +#. Description +#: ../dovecot-core.templates:4001 +msgid "Host name:" +msgstr "Computernaam" + +#. Type: string +#. Description +#: ../dovecot-core.templates:4001 +msgid "Please enter the host name to use in the SSL certificate." +msgstr "" +"Geef aan welke computernaam gebruikt moet worden in het SSL-certificaat." + +#. Type: string +#. Description +#: ../dovecot-core.templates:4001 +msgid "" +"It will become the \"commonName\" field of the generated SSL certificate." +msgstr "" +"Die komt terecht in het veld \"commonName\" van het SSL-certificaat dat " +"aangemaakt wordt." diff --git a/rules b/rules new file mode 100755 index 0000000..c13304a --- /dev/null +++ b/rules @@ -0,0 +1,250 @@ +#!/usr/bin/make -f + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +SHELL=/bin/bash -O extglob +PIGEONHOLE_DIR=$(CURDIR)/pigeonhole + +export DEB_BUILD_MAINT_OPTIONS=hardening=+all optimize=+lto + +# LP: 1636781 - strip incompatible default linker option +ifeq ($(shell dpkg-vendor --derives-from Ubuntu && echo yes),yes) + export DEB_LDFLAGS_MAINT_STRIP = -Wl,-Bsymbolic-functions +endif + +DOV_DEB_CFLAGS= +DOV_DEB_CXXFLAGS= +DOV_DEB_LDFLAGS= + +# Ensure that stacktrace generation works: +DOV_DEB_LDFLAGS += -rdynamic +include /usr/share/dpkg/architecture.mk +ifneq ($(filter armel armhf hppa mips64el mipsel riscv64 sparc64, $(DEB_HOST_ARCH)),) + DOV_DEB_CFLAGS += -funwind-tables + DOV_DEB_CXXFLAGS += -funwind-tables +endif + +# Extra hardening flags, not for hppa. +ifeq ($(filter hppa, $(DEB_HOST_ARCH)),) + DOV_DEB_CFLAGS += -fstack-clash-protection + DOV_DEB_CXXFLAGS += -fstack-clash-protection + DOV_DEB_LDFLAGS += -fstack-clash-protection +endif + +export DEB_CFLAGS_MAINT_APPEND = $(DOV_DEB_CFLAGS) +export DEB_CXXFLAGS_MAINT_APPEND = $(DOV_DEB_CXXFLAGS) +export DEB_LDFLAGS_MAINT_APPEND = $(DOV_DEB_LDFLAGS) + + +ifeq ($(DEB_HOST_ARCH_OS),linux) + CONFIGURE_APPARMOR = --with-apparmor +endif + +ifeq (i386,$(DEB_HOST_ARCH)) + CONFIGURE_Y2038=--disable-year2038 +else + CONFIGURE_Y2038=--enable-year2038 +endif + +CLEANFILES=src/lib-settings/settings-history-core.c \ + src/lib-dict-backend/dict-drivers-register.c \ + src/lib-sql/test-database.db \ + pigeonhole/src/plugins/settings/pigeonhole-settings-dynamic.c + +# Macros to help move files to the module packages +CORE_DIR=$(CURDIR)/debian/dovecot-core +PKG_DIR=$(CURDIR)/debian/dovecot-PKG +MOVE=install -d $(PKG_DIR:PKG=$(2))/$(dir $(1)); mv $(CORE_DIR)/$(1) $(PKG_DIR:PKG=$(2))/$(dir $(1)); + +# Files to move from dovecot-core to the individual packages. Files added to +# _files will be automatically moved from dovecot's tree to the +# respective package. +auth-lua_files = usr/lib/dovecot/modules/auth/libauthdb_lua.so + +imapd_files = usr/lib/dovecot/imap* \ + usr/lib/dovecot/modules/lib??_imap_!(*sieve*) \ + usr/share/dovecot/conf.d/??-imap* + +pop3d_files = usr/lib/dovecot/pop3* \ + usr/share/dovecot/conf.d/??-pop3* + +lmtpd_files = usr/lib/dovecot/lmtp* \ + usr/share/dovecot/conf.d/??-lmtp* + +managesieved_files = usr/lib/dovecot/managesieve* \ + usr/lib/dovecot/modules/settings/libmanagesieve* \ + usr/share/dovecot/conf.d/??-managesieve.conf + +submissiond_files = usr/lib/dovecot/submission* \ + usr/share/dovecot/conf.d/??-submission.conf + +pgsql_files = usr/lib/dovecot/modules/auth/libdriver_pgsql* \ + usr/lib/dovecot/modules/dict/libdriver_pgsql* \ + usr/lib/dovecot/modules/libdriver_pgsql* + +mysql_files = usr/lib/dovecot/modules/auth/libdriver_mysql* \ + usr/lib/dovecot/modules/dict/libdriver_mysql* \ + usr/lib/dovecot/modules/libdriver_mysql* + +sqlite_files = usr/lib/dovecot/modules/auth/libdriver_sqlite* \ + usr/lib/dovecot/modules/dict/libdriver_sqlite* \ + usr/lib/dovecot/modules/libdriver_sqlite* + +gssapi_files = usr/lib/dovecot/modules/auth/libmech_gssapi* + +ldap_files = usr/lib/dovecot/modules/auth/libauthdb_ldap* \ + usr/lib/dovecot/modules/dict/libdict_ldap* \ + usr/lib/dovecot/libdovecot-ldap* \ + usr/share/dovecot/*-ldap.conf.ext \ + usr/share/dovecot/conf.d/*-ldap.conf.ext + +sieve_files = usr/bin/sieve* \ + usr/lib/dovecot/modules/lib??_sieve* \ + usr/lib/dovecot/modules/lib??_imap_*sieve* \ + usr/lib/dovecot/modules/doveadm/lib??_doveadm_sieve* \ + usr/lib/dovecot/modules/sieve \ + usr/lib/dovecot/libdovecot-sieve* \ + usr/share/man/man1/sieve* \ + usr/share/dovecot/conf.d/??-sieve.conf \ + usr/share/dovecot/conf.d/??-sieve-extprograms.conf + +flatcurve_files = usr/lib/dovecot/modules/lib??_fts_flatcurve_* \ + usr/share/dovecot/conf.d/??-fts-flatcurve.conf + +solr_files = usr/lib/dovecot/modules/lib??_fts_solr_* + +dev_files = usr/include/* \ + usr/lib/dovecot/dovecot-config \ + usr/share/aclocal/*.m4 + +%: + dh $@ + +override_dh_autoreconf: + dh_autoreconf debian/autogen.sh + +override_dh_auto_configure: TEST_DIR_OVERRIDE=$(shell mktemp -d) +override_dh_auto_configure: + TEST_DIR_OVERRIDE='$(TEST_DIR_OVERRIDE)' KRB5CONFIG=krb5-config.mit systemdsystemunitdir=/usr/lib/systemd/system dh_auto_configure -- \ + --enable-experimental-mail-utf8 \ + --with-ldap=plugin \ + --with-ssl=openssl \ + --with-sql=plugin \ + --with-pgsql \ + --with-mysql \ + --with-sqlite \ + --with-gssapi=plugin \ + --with-solr \ + --with-flatcurve \ + --with-ioloop=best \ + --with-icu \ + $(CONFIGURE_APPARMOR) \ + --with-lz4 \ + --with-lua=plugin \ + --with-sodium \ + --libdir=\$${prefix}/lib \ + --libexecdir=\$${prefix}/lib \ + --docdir=\$${prefix}/share/doc/dovecot-core \ + --with-moduledir=\$${prefix}/lib/dovecot/modules \ + --with-rundir=/run/dovecot \ + --disable-rpath \ + --disable-static \ + $(CONFIGURE_Y2038) + +ifeq (i386,$(DEB_HOST_ARCH)) + sed -i 's/TIME_T_MAX_BITS 32/TIME_T_MAX_BITS 31/' config.h +endif + + $(MAKE) dovecot-config +# remove unreproducible '-ffile-prefix-map=/build/1st/dovecot-2.3.10.1+dfsg1=.' +# '-fdebug-prefix-map=/build/dovecot-O55xqn/dovecot-2.3.10.1+dfsg1=.' + sed -i 's/\s\+-ffile-prefix-map=\S\+=.\s\+/ /g' dovecot-config + sed -i 's/\s\+-fdebug-prefix-map=\S\+=.\s\+/ /g' dovecot-config + +# Pigeonhole + touch $(PIGEONHOLE_DIR)/stamp.h.in + dh_auto_configure -D $(PIGEONHOLE_DIR) -- \ + --with-dovecot=../ \ + --libdir=\$${prefix}/lib \ + --libexecdir=\$${prefix}/lib \ + --disable-static \ + --with-ldap=plugin + +override_dh_auto_build: +# Remove all-settings.c and let it be re-generated from the (patched) +# component settings files. + rm -f src/config/all-settings.c +# Create doc/stamp-man, which may have been removed on an earlier +# 'clean' invocation, so we don't try to download docs content from +# the internet: + touch doc/stamp-man + dh_auto_build + dh_auto_build -D $(PIGEONHOLE_DIR) + +override_dh_auto_clean: + dh_auto_clean + dh_auto_clean -D $(PIGEONHOLE_DIR) + -rm -f $(CLEANFILES) + +override_dh_auto_install: +# Install everything under dovecot-core + $(MAKE) install DESTDIR=$(CORE_DIR) + $(MAKE) -C $(PIGEONHOLE_DIR) install DESTDIR=$(CORE_DIR) + rm `find $(CURDIR)/debian -name '*.la'` + +override_dh_install: +# dh_auto_install has installed everything in the dovecot-core package. +# Run dh_install to install additional files from the source directory +# or debian/ to the individual packages. NOTE: there is nothing in +# debian/tmp! + dh_install + install -D -m 0755 $(CORE_DIR)/usr/share/doc/dovecot-core/mkcert.sh \ + $(CORE_DIR)/usr/share/dovecot/mkcert.sh + rm $(CORE_DIR)/usr/share/doc/dovecot-core/mkcert.sh + rm $(CORE_DIR)/usr/share/doc/dovecot-core/dovecot-openssl.cnf + rm $(CORE_DIR)/usr/share/doc/dovecot-core/solr-*.xml + +# We want the dovecot.conf managed by ucf, not the upstream default: + rm -f $(CORE_DIR)/etc/dovecot/dovecot.conf + cp -r debian/conf/* $(CORE_DIR)/usr/share/dovecot/ + $(foreach package,$(patsubst dovecot-%,%,$(shell dh_listpackages)),\ + $(foreach file,$($(package)_files),\ + $(call MOVE,$(file),$(package)))) + + rmdir $(CORE_DIR)/usr/include + rmdir $(CORE_DIR)/usr/share/aclocal + install -d $(PKG_DIR:PKG=dev)/usr/share/dovecot + grep ABI_VERSION config.h | awk '{print $$NF}' | tr -d \" \ + | tr '[A-Z]' '[a-z]' > $(PKG_DIR:PKG=dev)/usr/share/dovecot/dovecot-abi +# sanity check the ABI: + grep -F -q abi $(PKG_DIR:PKG=dev)/usr/share/dovecot/dovecot-abi + +# Install apport hook + install -D -m 644 debian/source_dovecot.py $(CURDIR)/debian/dovecot-core/usr/share/apport/package-hooks/dovecot-core.py + + install -D -m644 debian/dovecot-imapd.ufw.profile debian/dovecot-imapd/etc/ufw/applications.d/dovecot-imapd + install -D -m644 debian/dovecot-pop3d.ufw.profile debian/dovecot-pop3d/etc/ufw/applications.d/dovecot-pop3d + +override_dh_installpam: + dh_installpam -pdovecot-core --name=dovecot + +override_dh_installinit: + dh_installinit -pdovecot-core --name=dovecot -u"defaults 20" + +override_dh_installsystemd: + dh_installsystemd -pdovecot-core dovecot.service + dh_installsystemd -pdovecot-core --no-enable dovecot.socket + +override_dh_gencontrol: + dh_gencontrol -- -Vdovecot:ABI-Version=$(shell cat $(CURDIR)/debian/dovecot-dev/usr/share/dovecot/dovecot-abi) + +override_dh_makeshlibs: +# Do not add an ldconfig trigger; none of the dovecot shared libraries +# are public. + dh_makeshlibs -n + +override_dh_installdocs: + dh_installdocs + cp pigeonhole/README $(CORE_DIR)/usr/share/doc/dovecot-core/pigeonhole.README + cp pigeonhole/NEWS $(CORE_DIR)/usr/share/doc/dovecot-core/pigeonhole.NEWS diff --git a/salsa-ci.yml b/salsa-ci.yml new file mode 100644 index 0000000..aa4b356 --- /dev/null +++ b/salsa-ci.yml @@ -0,0 +1,9 @@ +--- +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml + +# The test suite does not pass reprotest +variables: + SALSA_CI_DISABLE_REPROTEST: 1 + SALSA_CI_DISABLE_CROSSBUILD_ARM64: 1 + SALSA_CI_DISABLE_BUILD_PACKAGE_TWICE: 1 diff --git a/source/format b/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/source/lintian-overrides b/source/lintian-overrides new file mode 100644 index 0000000..d4d4b5b --- /dev/null +++ b/source/lintian-overrides @@ -0,0 +1 @@ +dovecot source: intra-source-package-circular-dependency dovecot-core dovecot-sieve diff --git a/source_dovecot.py b/source_dovecot.py new file mode 100644 index 0000000..84fb839 --- /dev/null +++ b/source_dovecot.py @@ -0,0 +1,38 @@ +#!/usr/bin/python3 + +'''Dovecot Apport interface + +Copyright (C) 2010 Canonical Ltd/ +Author: Chuck Short + +This program is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 2 of the License, or (at your +option) any later version. See http://www.gnu.org/copyleft/gpl.html for +the full text of the license. +''' + +from apport.hookutils import * + +def add_info(report, ui): + response = ui.yesno("The output of dovecot -n may help developers diagnose your bug more quickly, however, it may contain sensitive information. Do you want to include it in your bug report?") + + if response is None: #user canceled + raise StopIteration + + elif response: + report['DovecotConf'] = root_command_output(['/usr/sbin/dovecot', '-n']) + + + elif not response: + ui.information("The contents of dovecot -n will NOT be includeded in the bug report") + + packages = ['dovecot-common', 'dovecot-core', 'dovecot-dev', 'dovecot-pop3d', 'dovecot-imapd', 'mail-stack-delivery', 'dovecot-postfix'] + versions = '' + for package in packages: + try: + version = package.get_version(package) + except: + version = 'N/A' + versions += '%s %s\n' %(package, version) + report['DovecotInstalledVersions'] = versions diff --git a/tests/control b/tests/control new file mode 100644 index 0000000..fb703d6 --- /dev/null +++ b/tests/control @@ -0,0 +1,14 @@ +Tests: doveadm +Depends: dovecot-core +Restrictions: needs-root + +Tests: systemd +Depends: dovecot-core, systemd-sysv + +Test-Command: run-parts --report --exit-on-error debian/tests/usage +Depends: dovecot-imapd, dovecot-pop3d, dovecot-managesieved, sieve-connect, python3 +Restrictions: needs-root, breaks-testbed, allow-stderr + +Tests: testmails +Restrictions: needs-root, breaks-testbed +Depends: dovecot-imapd, dovecot-pop3d, lsb-release, python3, python3-passlib diff --git a/tests/doveadm b/tests/doveadm new file mode 100755 index 0000000..9e2bd04 --- /dev/null +++ b/tests/doveadm @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +echo "Checking if dovecot is running" +doveadm -f flow instance list | grep -q 'running=yes' +echo "OK" diff --git a/tests/systemd b/tests/systemd new file mode 100755 index 0000000..48afbf5 --- /dev/null +++ b/tests/systemd @@ -0,0 +1,22 @@ +#!/bin/sh + +set -e + +echo "Checking whether dovecot.service is enabled" +systemctl is-enabled dovecot.service + +echo "Checking whether dovecot.service is a native unit" +source=$(systemctl show -pSourcePath dovecot.service | cut -d = -f 2) +if [ -n "$source" ]; then + echo $source + exit 1 +else + echo "OK (no SourcePath found)" +fi + +echo "Checking whether dovecot.socket is inactive" +status=$(systemctl show -pActiveState dovecot.socket | cut -d = -f 2) +echo $status +if [ "$status" != inactive ]; then + exit 1 +fi diff --git a/tests/testmails b/tests/testmails new file mode 100755 index 0000000..67d5f3b --- /dev/null +++ b/tests/testmails @@ -0,0 +1,254 @@ +#!/usr/bin/python3 + +import grp +import imaplib +import os +import os.path +import poplib +import pwd +import random +import string +import subprocess +import sys +import unittest + +from passlib.hash import des_crypt + + +def random_string(length): + '''Return a random string, consisting of ASCII letters, with given + length.''' + + s = '' + maxind = len(string.ascii_letters)-1 + for _ in range(length): + s += string.ascii_letters[random.randint(0, maxind)] + return s.lower() + + +def login_exists(login): + '''Checks whether the given login exists on the system.''' + + try: + pwd.getpwnam(login) + return True + except KeyError: + return False + + +def get_distribution(): + '''Return the name of the Linux Distribution we are running.''' + cmd = ['lsb_release', '-si'] + output = subprocess.check_output(cmd) + return output.strip() + + +class TestUser: + '''Create a temporary test user and remove it again on close.''' + + def __init__(self): + '''Create a new user account with a random password.''' + + self.login = None + + while True: + login = random_string(8) + if not login_exists(login): + break + + self.salt = random_string(2) + self.password = random_string(8) + self.crypted = des_crypt.using(salt=self.salt).hash(self.password) + + subprocess.check_call(['useradd', '-p', self.crypted, '-m', login]) + + self.login = login + p = pwd.getpwnam(self.login) + self.uid = p[2] + self.gid = p[3] + + def __del__(self): + '''Remove the created user account.''' + + if self.login: + self.close() + + def close(self): + '''Remove the created user account.''' + + subprocess.check_call(['userdel', '-f', '-r', self.login]) + self.login = None + + +class DovecotBasics(unittest.TestCase): + '''Base operational tests for Dovecot server.''' + + def setUp(self): + '''Create test scenario. + + We want to test the default setup, but pre-setup an mbox on a tmp user + ''' + + self.distribution = get_distribution() + self.user = TestUser() + + # create fresh test mailbox with one new and one old mail + self.mailbox = '/var/mail/' + self.user.login + self.orig_mbox = '''From test1@test1.com Fri Nov 17 02:21:08 2006 +Date: Thu, 16 Nov 2006 17:12:23 -0800 +From: Test User 1 +To: Dovecot tester +Subject: Test 1 +Status: N + +Some really important news. + +From test2@test1.com Tue Nov 28 11:29:34 2006 +Date: Tue, 28 Nov 2006 11:29:34 +0100 +From: Test User 2 +To: Dovecot tester +Subject: Test 2 +Status: R + +More news. + +Get cracking! +''' + with open(self.mailbox, 'w') as f: + f.write(self.orig_mbox) + os.chown(self.mailbox, self.user.uid, grp.getgrnam('mail')[2]) + os.chmod(self.mailbox, 0o660) + + def tearDown(self): + self.user.close() + + def _test_pop3_proto(self, pop): + '''Internal factorization of POP3 protocol checks with an established + connection.''' + + # check empty password + self.assertEqual(pop.user(self.user.login), b'+OK') + self.assertRaises(poplib.error_proto, pop.pass_, '') + + # check wrong password + self.assertEqual(pop.user(self.user.login), b'+OK') + self.assertRaises(poplib.error_proto, pop.pass_, '123') + + # check correct password + self.assertEqual(pop.user(self.user.login), b'+OK') + self.assertEqual(pop.pass_(self.user.password), b'+OK Logged in.') + + # check messages + self.assertEqual(pop.stat()[0], 2, b'2 available messages') + self.assertEqual(pop.list()[1], [b'1 163', b'2 161']) + self.assertEqual('\n'.join(l.decode() for l in pop.retr(1)[1]), '''Date: Thu, 16 Nov 2006 17:12:23 -0800 +From: Test User 1 +To: Dovecot tester +Subject: Test 1 + +Some really important news.''') + self.assertEqual('\n'.join(l.decode() for l in pop.retr(2)[1]), '''Date: Tue, 28 Nov 2006 11:29:34 +0100 +From: Test User 2 +To: Dovecot tester +Subject: Test 2 + +More news. + +Get cracking!''') + + self.assertEqual(pop.quit(), b'+OK Logging out.') + + def test_pop3(self): + '''Test POP3 protocol.''' + + pop = poplib.POP3('localhost') + self.assertEqual(pop.getwelcome(), b'+OK Dovecot ready.') + + self._test_pop3_proto(pop) + + def test_pop3s(self): + '''Test POP3S protocol.''' + + pop = poplib.POP3_SSL('localhost') + self.assertEqual(pop.getwelcome(), b'+OK Dovecot ready.') + + self._test_pop3_proto(pop) + + def _test_imap_proto(self, imap): + '''Internal factorization of IMAP4 protocol checks with an established + connection.''' + + # invalid passwords + self.assertRaises(imaplib.IMAP4.error, imap.login, + self.user.login, '') + self.assertRaises(imaplib.IMAP4.error, imap.login, + self.user.login, '123') + + # correct password + imap.login(self.user.login, self.user.password) + + # list mailboxes + status, imlist = imap.list() + self.assertEqual(status, 'OK') + self.assertTrue(imlist[0].decode().endswith('INBOX')) + + # check mails + imap.select() + self.assertEqual(imap.search(None, 'ALL'), ('OK', [b'1 2'])) + self.assertEqual(imap.fetch('1', '(FLAGS)'), + ('OK', [b'1 (FLAGS (\\Recent))'])) + self.assertEqual(imap.fetch('2', '(FLAGS)'), + ('OK', [b'2 (FLAGS (\\Seen \\Recent))'])) + self.assertEqual(imap.fetch('1', '(BODY[TEXT])')[1][0][1], + b'Some really important news.\r\n') + self.assertEqual(imap.fetch('2', '(BODY[TEXT])')[1][0][1], + b'More news.\r\n\r\nGet cracking!') + + self.assertEqual(imap.fetch('1', '(RFC822)')[1], + [(b'1 (RFC822 {163}', + b'''Date: Thu, 16 Nov 2006 17:12:23 -0800\r +From: Test User 1 \r +To: Dovecot tester \r +Subject: Test 1\r +\r +Some really important news.\r +'''), b')']) + + # delete mail 1 + self.assertEqual(imap.store('1', '+FLAGS', '\\Deleted')[0], 'OK') + self.assertEqual(imap.expunge()[0], 'OK') + self.assertEqual(imap.search(None, 'ALL'), ('OK', [b'1'])) + + # old mail 2 is mail 1 now + self.assertEqual(imap.fetch('1', '(RFC822)')[1], + [(b'1 (RFC822 {161}', + b'''Date: Tue, 28 Nov 2006 11:29:34 +0100\r +From: Test User 2 \r +To: Dovecot tester \r +Subject: Test 2\r +\r +More news.\r +\r +Get cracking!'''), b')']) + imap.close() + imap.logout() + + def test_imap(self): + '''Test IMAP4 protocol.''' + + imap = imaplib.IMAP4('localhost') + self._test_imap_proto(imap) + + def test_imaps(self): + '''Test IMAP4S protocol.''' + + imap = imaplib.IMAP4_SSL('localhost') + self._test_imap_proto(imap) + + +if __name__ == '__main__': + os.dup2(1, 2) + suite = unittest.TestSuite() + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DovecotBasics)) + result = unittest.TextTestRunner(verbosity=2).run(suite) + sys.exit(not result.wasSuccessful()) diff --git a/tests/usage/00_setup b/tests/usage/00_setup new file mode 100755 index 0000000..b802eac --- /dev/null +++ b/tests/usage/00_setup @@ -0,0 +1,57 @@ +#!/bin/sh + +set -e + +echo "Setting up dovecot for the test" +# Move aside 10-auth.conf to disable passwd-based auth +if [ -f /etc/dovecot/conf.d/10-auth.conf ]; then + mv /etc/dovecot/conf.d/10-auth.conf /etc/dovecot/conf.d/10-auth.conf.bak +fi + +cat >/etc/dovecot/local.conf <<-EOF +mail_driver = maildir +mail_path = ~/Maildir +mail_inbox_path = ~/Maildir + +auth_mechanisms = plain + +passdb static { + driver = static + passdb_static_password = test +} + +userdb static { + driver = static + fields { + uid=nobody + gid=nogroup + home=/srv/dovecot-dep8/%u + } +} +EOF + +mkdir -p /srv/dovecot-dep8 +chown nobody:nogroup /srv/dovecot-dep8 + +echo "Restarting the service" +systemctl restart dovecot + +echo "Sending a test message via the LDA" +/usr/lib/dovecot/dovecot-lda -f "test@example.com" -d dep8 < +Message-Id: +From: Test User +To: dep8 +Subject: DEP-8 test + +This is just a test +EOF + +echo "Verifying that the email was correctly delivered" +if [ -z "$(doveadm search -u dep8 header message-id dep8-test-1@debian.org)" ]; then + echo "Message not found" + exit 1 +fi + +echo "Done" +echo diff --git a/tests/usage/imap b/tests/usage/imap new file mode 100755 index 0000000..5bdede7 --- /dev/null +++ b/tests/usage/imap @@ -0,0 +1,38 @@ +#!/usr/bin/python3 +import imaplib + +imaplib.Debug = 4 + +print("Testing IMAP") +print("Connecting") +client = imaplib.IMAP4('localhost') + +print("Checking for STARTTLS capability") +assert 'STARTTLS' in client.capabilities + +client.starttls() + +print("Logging in") +client.login('dep8', 'test') + +print("Selecting INBOX") +client.select() + +print("Looking for the test message") +res, uids = client.search(None, 'HEADER', 'MESSAGE-ID', '""') + +assert res == 'OK' +assert len(uids[0]) > 0 + +uid = uids[0].split()[0] + +print("Fetching and verifying test message") +res, data = client.fetch(uid, '(RFC822)') + +assert res == 'OK' + +lines = data[0][1].splitlines() + +assert b'Subject: DEP-8 test' in lines + +print("Done") diff --git a/tests/usage/pop3 b/tests/usage/pop3 new file mode 100755 index 0000000..00b1657 --- /dev/null +++ b/tests/usage/pop3 @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +import poplib + +print("Testing POP3") +print("Connecting") +client = poplib.POP3('localhost') +client.set_debuglevel(2) + +print("Checking for STARTTLS capability") +assert 'STLS' in client.capa() + +client.stls() + +print("Logging in") +client.user('dep8') +client.pass_('test') + +print("Listing INBOX") +res, data, _ = client.list() +assert res.startswith(b'+OK') + +print("Fetching and verifying test message") +for entry in data: + _id, _ = entry.split(maxsplit=1) + res, body, _ = client.retr(int(_id)) + if b'Subject: DEP-8 test' in body: + break +else: + raise AssertionError("Test message not found") + +print("Done") diff --git a/tests/usage/sieve b/tests/usage/sieve new file mode 100755 index 0000000..9ef68dd --- /dev/null +++ b/tests/usage/sieve @@ -0,0 +1,8 @@ +#!/usr/bin/bash +# Validate managesieved authentication + +user=dep8 +pass=test + +sieve-connect --notlsverify --debug -s localhost \ + -p 4190 -u "$user" --list --passwordfd 3 3< <(echo "$pass") diff --git a/upstream/metadata b/upstream/metadata new file mode 100644 index 0000000..02236b7 --- /dev/null +++ b/upstream/metadata @@ -0,0 +1,5 @@ +Name: dovecot +Repository: https://github.com/dovecot/core +Repository-Browse: https://github.com/dovecot/core +Contact: Dovecot Mailing List +Bug-Submit: Dovecot Mailing List diff --git a/upstream/signing-key.asc b/upstream/signing-key.asc new file mode 100644 index 0000000..66bc6ad --- /dev/null +++ b/upstream/signing-key.asc @@ -0,0 +1,25 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZ49ZxhYJKwYBBAHaRw8BAQdABhVJFqsoq7gZeXhY/xcz5Vc0i+vM/Df8RDSW +lzPHsoC0MkRvdmVjb3QgQ29tbXVuaXR5IEVkaXRpb24gPGRvdmVjb3QtY2VAZG92 +ZWNvdC5vcmc+iJAEExYIADgWIQTvCIIHn9TtMr+LI7KhsJ74TtxSGQUCZ49ZxgIb +AwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRChsJ74TtxSGUgvAP9gNC6pZKBV +micPYvE2UOTvX8WoEcjWI1UptTxFsHXOjwEA6DZiPoPn9Vvl4PuyG8TavjDslOL8 +pfn+XKZho2WoAgSJAjMEEAEKAB0WIQQr50qrPudU37nIDTMYo0iu7UCdoQUCZ49a +xgAKCRAYo0iu7UCdoVPOD/452ZnhEYW2/VxgDmD7DNaaxdfL/vKifc9MP2sJm+a2 +T95zHpm/erV2xw3gPpKcLuMaPhkexf6MMhoh9GnUnwEuoOfMWhMAn26RNPFvuUL0 +a73qoxRY9D+Q/PUNjNsNFj/3c8VIC3DeQhO77wVblLWr9+7J9GrzKtBCkb7zC5Xf +8zjLkCWZ15S0kVKNvBsZzEzLYVN/+Kk1s1WP4x1gnQdPm8N7+b4XAQSOUXLjkL5S +vGyIUuHwnYHQJeH51L+q8SpzEr59K9OiVYOCm0ICl28xjOjIC9Ym7a8W5Xa5new3 +ScxNeTuxRItYeDyxXi+YIYGHWYGjmppmlq3VksXirUHfgNoipVSbmGxCukShSsfH +f/40iO8DRFRnsUN2lzN4TCcT1ayQvKoyMFuoXQJonuLdbc7E+YLNIPsW66+aoAuW +g+5j2QFMFxAJS9IoTCsVQv52MuCj2X5KgwmBcYSgWGpoog9Yx+aaVQQ+X++9FX8r +RcnyOGOBaA6htH4g1kUOWwZ/NxObRiA7QLo4Rz6CJrU7ueRIaeSCCXTxCb5dJDeE +IB1MZh1artKZ6Rko+cv/Vy0Ojb0JfPDDoQyhblPIXhlJN8/4zFCjxilMDA9CTAB6 +kY1TVGp138CDnJHZvCdvbrlWf7ktw3+CGKQsZq96Jf756L28dTA42E+WqCBFMZna +V7g4BGePWcYSCisGAQQBl1UBBQEBB0A50U//EHUvujYRMNqQ3XMVt8lxU7HtdmsH +Bamh6BjmLgMBCAeIeAQYFggAIBYhBO8Iggef1O0yv4sjsqGwnvhO3FIZBQJnj1nG +AhsMAAoJEKGwnvhO3FIZEZQBAKuMI//Cjl38pD68fQfcnIMDV2YnXjvaswe7jbDr +Jyo4AQDr/VanhFwPxSlZ3MKY4PWyLw4Fm9mTT2ofcluFttRsDg== +=eQ4x +-----END PGP PUBLIC KEY BLOCK----- diff --git a/watch b/watch new file mode 100644 index 0000000..3d07aed --- /dev/null +++ b/watch @@ -0,0 +1,8 @@ +# watch control file for uscan +version=4 +# core +opts="pgpmode=auto, dversionmangle=s/\+dfsg\d*//, uversionmangle=s/\.-[0-9]+//, oversionmangle=s/(.*)/$1+dfsg1/" \ + https://dovecot.org/releases/2.4/dovecot-([\.0-9]+)(-[0-9]+)?\.tar\.gz debian +# pigeonhole +opts="pgpmode=auto, dversionmangle=s/\+dfsg\d*//, uversionmangle=s/\.-[0-9]//, oversionmangle=s/(.*)/$1+dfsg1/, component=pigeonhole" \ + https://pigeonhole.dovecot.org/releases/2.4/dovecot-pigeonhole-([\.0-9]+)(-[0-9]+)?\.tar\.gz ignore uupdate -- 2.39.5